From a6603a2e0a183a90f6d76fa54a81ca9150615c04 Mon Sep 17 00:00:00 2001 From: pascal Date: Mon, 27 Jul 2026 15:17:30 +0200 Subject: [PATCH] have explicit network map data type and do calculation from there --- .../network_map/controller/controller.go | 4 +- .../shared/grpc/components_encoder.go | 52 +- .../shared/grpc/components_encoder_test.go | 98 ++- .../grpc/components_envelope_response.go | 11 +- .../grpc/components_envelope_response_test.go | 78 +-- .../internals/shared/grpc/conversion.go | 3 +- management/internals/shared/grpc/server.go | 2 +- .../http/handlers/peers/peers_handler.go | 19 +- management/server/peer.go | 3 +- management/server/peer_test.go | 9 +- management/server/route_test.go | 2 +- management/server/types/account.go | 65 -- management/server/types/account_components.go | 622 +---------------- .../server/types/account_networkmapdata.go | 515 ++++++++++++++ .../types/account_private_netmap_test.go | 4 +- management/server/types/account_test.go | 514 -------------- management/server/types/aliases.go | 10 +- management/server/types/ipv6_endtoend_test.go | 3 +- .../types/networkmap_components_test.go | 5 +- shared/management/networkmap/decode.go | 121 ++-- shared/management/networkmap/encode.go | 9 +- shared/management/networkmap/envelope_test.go | 70 +- .../networkmap/networkmapcompute.go | 659 ++++++++++++++++++ .../management/networkmap/networkmapdata.go | 55 ++ .../networkmap/nmdata/account_settings.go | 11 + shared/management/networkmap/nmdata/dns.go | 18 + .../networkmap/nmdata/dns_settings.go | 6 + shared/management/networkmap/nmdata/group.go | 24 + .../networkmap/nmdata/nameserver.go | 24 + .../management/networkmap/nmdata/network.go | 16 + .../networkmap/nmdata/network_resource.go | 18 + .../networkmap/nmdata/network_router.go | 10 + shared/management/networkmap/nmdata/peer.go | 105 +++ shared/management/networkmap/nmdata/policy.go | 93 +++ .../management/networkmap/nmdata/posture.go | 59 ++ .../networkmap/nmdata/posture_geo_location.go | 45 ++ .../networkmap/nmdata/posture_nb_version.go | 38 + .../networkmap/nmdata/posture_network.go | 62 ++ .../networkmap/nmdata/posture_os_version.go | 79 +++ .../networkmap/nmdata/posture_process.go | 56 ++ shared/management/networkmap/nmdata/route.go | 108 +++ shared/management/types/firewall_helpers.go | 12 +- shared/management/types/firewall_rule.go | 22 +- shared/management/types/firewall_rule_test.go | 69 +- shared/management/types/network.go | 15 +- .../management/types/networkmap_components.go | 193 +++-- .../types/networkmap_components_compact.go | 51 +- shared/management/types/nmdata_convert.go | 70 ++ 48 files changed, 2505 insertions(+), 1632 deletions(-) create mode 100644 management/server/types/account_networkmapdata.go create mode 100644 shared/management/networkmap/networkmapcompute.go create mode 100644 shared/management/networkmap/networkmapdata.go create mode 100644 shared/management/networkmap/nmdata/account_settings.go create mode 100644 shared/management/networkmap/nmdata/dns.go create mode 100644 shared/management/networkmap/nmdata/dns_settings.go create mode 100644 shared/management/networkmap/nmdata/group.go create mode 100644 shared/management/networkmap/nmdata/nameserver.go create mode 100644 shared/management/networkmap/nmdata/network.go create mode 100644 shared/management/networkmap/nmdata/network_resource.go create mode 100644 shared/management/networkmap/nmdata/network_router.go create mode 100644 shared/management/networkmap/nmdata/peer.go create mode 100644 shared/management/networkmap/nmdata/policy.go create mode 100644 shared/management/networkmap/nmdata/posture.go create mode 100644 shared/management/networkmap/nmdata/posture_geo_location.go create mode 100644 shared/management/networkmap/nmdata/posture_nb_version.go create mode 100644 shared/management/networkmap/nmdata/posture_network.go create mode 100644 shared/management/networkmap/nmdata/posture_os_version.go create mode 100644 shared/management/networkmap/nmdata/posture_process.go create mode 100644 shared/management/networkmap/nmdata/route.go create mode 100644 shared/management/types/nmdata_convert.go diff --git a/management/internals/controllers/network_map/controller/controller.go b/management/internals/controllers/network_map/controller/controller.go index 5785004db..8d0c4269d 100644 --- a/management/internals/controllers/network_map/controller/controller.go +++ b/management/internals/controllers/network_map/controller/controller.go @@ -641,7 +641,7 @@ func (c *Controller) GetValidatedPeerWithComponents(ctx context.Context, isRequi if err != nil { return nil, nil, nil, nil, 0, err } - return peer, &types.NetworkMapComponents{Network: network.Copy()}, nil, nil, 0, nil + return peer, &types.NetworkMapComponents{Network: types.TwinNetwork(network)}, nil, nil, 0, nil } account, err := c.requestBuffer.GetAccountWithBackpressure(ctx, accountID) @@ -794,7 +794,7 @@ func (c *Controller) GetValidatedPeerWithMap(ctx context.Context, isRequiresAppr } emptyMap := &types.NetworkMap{ - Network: network.Copy(), + Network: types.TwinNetwork(network), } return emptyMap, nil, 0, nil } diff --git a/management/internals/shared/grpc/components_encoder.go b/management/internals/shared/grpc/components_encoder.go index d7b787464..411f61184 100644 --- a/management/internals/shared/grpc/components_encoder.go +++ b/management/internals/shared/grpc/components_encoder.go @@ -4,13 +4,9 @@ import ( "encoding/base64" "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" + nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" ) @@ -131,7 +127,7 @@ func EncodeNetworkMapEnvelope(in ComponentsEnvelopeInput) *proto.NetworkMapEnvel // networkSerial returns c.Network.CurrentSerial() with a nil guard. The // production path always populates c.Network, but the encoder is exported // and a hand-built components struct may omit it. -func networkSerial(n *types.Network) uint64 { +func networkSerial(n *nmdata.Network) uint64 { if n == nil { return 0 } @@ -166,7 +162,7 @@ func (e *componentEncoder) indexAllPeers() { } } -func (e *componentEncoder) appendPeer(p *nbpeer.Peer) uint32 { +func (e *componentEncoder) appendPeer(p *nmdata.Peer) uint32 { if idx, ok := e.peerOrder[p.ID]; ok { return idx } @@ -180,7 +176,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]*nmdata.Peer) []uint32 { if len(routers) == 0 { return nil } @@ -220,7 +216,7 @@ func (e *componentEncoder) encodeGroups() []*proto.GroupCompact { // list and a map from policy pointer to the indexes of its emitted rules in // that list — used by encodeResourcePoliciesMap to translate // ResourcePoliciesMap[resourceID][]*Policy into wire-side indexes. -func (e *componentEncoder) encodePolicies(policies []*types.Policy) []*proto.PolicyCompact { +func (e *componentEncoder) encodePolicies(policies []*nmdata.Policy) []*proto.PolicyCompact { if len(policies) == 0 { return nil } @@ -242,7 +238,7 @@ func (e *componentEncoder) encodePolicies(policies []*types.Policy) []*proto.Pol } // encodePolicyRule maps a single PolicyRule under pol to a PolicyCompact entry. -func (e *componentEncoder) encodePolicyRule(pol *types.Policy, r *types.PolicyRule) *proto.PolicyCompact { +func (e *componentEncoder) encodePolicyRule(pol *nmdata.Policy, r *nmdata.PolicyRule) *proto.PolicyCompact { return &proto.PolicyCompact{ Id: pol.PublicID, Action: networkmap.GetProtoAction(string(r.Action)), @@ -281,14 +277,14 @@ func (e *componentEncoder) groupPublicXids(src []string) []string { // only live in ResourcePoliciesMap; without this union step they'd be lost // from the wire and the client's resource-policy lookup would come back // empty. -func unionPolicies(policies []*types.Policy, resourcePolicies map[string][]*types.Policy) []*types.Policy { +func unionPolicies(policies []*nmdata.Policy, resourcePolicies map[string][]*nmdata.Policy) []*nmdata.Policy { // Fast path: non-router peers have no resource-only policies, so the // "union" is identical to `policies`. Skip the dedup map allocation. if len(resourcePolicies) == 0 { return policies } seen := make(map[string]struct{}, len(policies)) - out := make([]*types.Policy, 0, len(policies)) + out := make([]*nmdata.Policy, 0, len(policies)) for _, p := range policies { if p == nil { continue @@ -346,12 +342,12 @@ func (e *componentEncoder) groupPublicXid(groupID string) (string, bool) { // peers array. For other resource types only the type string is shipped // today (Calculate's resource-typed rule path consults SourceResource only // for "peer" — other types fall through to group-based lookup). -func (e *componentEncoder) resourceToProto(r types.Resource) *proto.ResourceCompact { +func (e *componentEncoder) resourceToProto(r nmdata.Resource) *proto.ResourceCompact { if r.ID == "" && r.Type == "" { return nil } - out := &proto.ResourceCompact{Type: string(r.Type)} - if r.Type == types.ResourceTypePeer && r.ID != "" { + out := &proto.ResourceCompact{Type: r.Type} + if r.Type == string(types.ResourceTypePeer) && r.ID != "" { if idx, ok := e.peerOrder[r.ID]; ok { out.PeerIndexSet = true out.PeerIndex = idx @@ -390,7 +386,7 @@ func (e *componentEncoder) networkPublicId(xid string) (string, bool) { return id, true } -func (e *componentEncoder) encodeDNSSettings(s *types.DNSSettings) *proto.DNSSettingsCompact { +func (e *componentEncoder) encodeDNSSettings(s *nmdata.DNSSettings) *proto.DNSSettingsCompact { if s == nil || len(s.DisabledManagementGroups) == 0 { return nil } @@ -405,7 +401,7 @@ func (e *componentEncoder) encodeDNSSettings(s *types.DNSSettings) *proto.DNSSet return out } -func (e *componentEncoder) encodeRoutes(routes []*nbroute.Route) []*proto.RouteRaw { +func (e *componentEncoder) encodeRoutes(routes []*nmdata.Route) []*proto.RouteRaw { if len(routes) == 0 { return nil } @@ -443,7 +439,7 @@ func (e *componentEncoder) encodeRoutes(routes []*nbroute.Route) []*proto.RouteR return out } -func (e *componentEncoder) encodeNameServerGroups(nsgs []*nbdns.NameServerGroup) []*proto.NameServerGroupRaw { +func (e *componentEncoder) encodeNameServerGroups(nsgs []*nmdata.NameServerGroup) []*proto.NameServerGroupRaw { if len(nsgs) == 0 { return nil } @@ -466,7 +462,7 @@ func (e *componentEncoder) encodeNameServerGroups(nsgs []*nbdns.NameServerGroup) return out } -func encodeNameServers(servers []nbdns.NameServer) []*proto.NameServer { +func encodeNameServers(servers []nmdata.NameServer) []*proto.NameServer { if len(servers) == 0 { return nil } @@ -481,7 +477,7 @@ func encodeNameServers(servers []nbdns.NameServer) []*proto.NameServer { return out } -func encodeSimpleRecords(records []nbdns.SimpleRecord) []*proto.SimpleRecord { +func encodeSimpleRecords(records []nmdata.SimpleRecord) []*proto.SimpleRecord { if len(records) == 0 { return nil } @@ -498,7 +494,7 @@ func encodeSimpleRecords(records []nbdns.SimpleRecord) []*proto.SimpleRecord { return out } -func encodeCustomZones(zones []nbdns.CustomZone) []*proto.CustomZone { +func encodeCustomZones(zones []nmdata.CustomZone) []*proto.CustomZone { if len(zones) == 0 { return nil } @@ -514,7 +510,7 @@ func encodeCustomZones(zones []nbdns.CustomZone) []*proto.CustomZone { return out } -func (e *componentEncoder) encodeNetworkResources(resources []*resourceTypes.NetworkResource) []*proto.NetworkResourceRaw { +func (e *componentEncoder) encodeNetworkResources(resources []*nmdata.NetworkResource) []*proto.NetworkResourceRaw { if len(resources) == 0 { return nil } @@ -543,7 +539,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]*nmdata.NetworkRouter) map[string]*proto.NetworkRouterList { if len(routersMap) == 0 { return nil } @@ -579,7 +575,7 @@ func (e *componentEncoder) encodeRoutersMap(routersMap map[string]map[string]*ro return out } -func (e *componentEncoder) encodeResourcePoliciesMap(rpm map[string][]*types.Policy) map[string]*proto.PolicyIds { +func (e *componentEncoder) encodeResourcePoliciesMap(rpm map[string][]*nmdata.Policy) map[string]*proto.PolicyIds { if len(rpm) == 0 { return nil } @@ -666,7 +662,7 @@ func (e *componentEncoder) encodePostureFailedPeers(m map[string]map[string]stru // (which shouldn't happen in production but the encoder is exported) // degrades to login_expiration_enabled = false, which makes // LoginExpired() return false for every peer. -func toAccountSettingsCompact(s *types.AccountSettingsInfo) *proto.AccountSettingsCompact { +func toAccountSettingsCompact(s *nmdata.AccountSettingsInfo) *proto.AccountSettingsCompact { if s == nil { return &proto.AccountSettingsCompact{} } @@ -676,7 +672,7 @@ func toAccountSettingsCompact(s *types.AccountSettingsInfo) *proto.AccountSettin } } -func toAccountNetwork(n *types.Network) *proto.AccountNetwork { +func toAccountNetwork(n *nmdata.Network) *proto.AccountNetwork { if n == nil { return nil } @@ -692,7 +688,7 @@ func toAccountNetwork(n *types.Network) *proto.AccountNetwork { return out } -func toPeerCompact(p *nbpeer.Peer) *proto.PeerCompact { +func toPeerCompact(p *nmdata.Peer) *proto.PeerCompact { pc := &proto.PeerCompact{ WgPubKey: decodeWgKey(p.Key), SshPubKey: []byte(p.SSHKey), @@ -754,7 +750,7 @@ func portsToUint32(ports []string) []uint32 { return out } -func portRangesToProto(ranges []types.RulePortRange) []*proto.PortInfo_Range { +func portRangesToProto(ranges []nmdata.RulePortRange) []*proto.PortInfo_Range { if len(ranges) == 0 { return nil } diff --git a/management/internals/shared/grpc/components_encoder_test.go b/management/internals/shared/grpc/components_encoder_test.go index d82bba362..bff757577 100644 --- a/management/internals/shared/grpc/components_encoder_test.go +++ b/management/internals/shared/grpc/components_encoder_test.go @@ -15,11 +15,8 @@ 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/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" ) @@ -155,67 +152,66 @@ func envelopesEquivalent(a, b *proto.NetworkMapEnvelope) bool { } func newTestComponents() *types.NetworkMapComponents { - peerA := &nbpeer.Peer{ + peerA := &nmdata.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"}, + Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, } - peerB := &nbpeer.Peer{ + peerB := &nmdata.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"}, + Meta: nmdata.PeerSystemMeta{WtVersion: "0.25.0"}, } - peerC := &nbpeer.Peer{ + peerC := &nmdata.Peer{ ID: "peer-c", Key: testWgKeyC, IP: netip.AddrFrom4([4]byte{100, 64, 0, 3}), DNSLabel: "peerc", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, } return &types.NetworkMapComponents{ PeerID: "peer-a", - Network: &types.Network{ + Network: &nmdata.Network{ Identifier: "net-test", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}, Serial: 7, }, - AccountSettings: &types.AccountSettingsInfo{ + AccountSettings: &nmdata.AccountSettingsInfo{ PeerLoginExpirationEnabled: true, PeerLoginExpiration: 2 * time.Hour, }, - Peers: map[string]*nbpeer.Peer{ + Peers: map[string]*nmdata.Peer{ "peer-a": peerA, "peer-b": peerB, "peer-c": peerC, }, - Groups: map[string]*types.Group{ - "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"}}, + Groups: map[string]*nmdata.Group{ + "group-src": {PublicID: "1", Name: "Src", Peers: []string{"peer-a"}}, + "group-dst": {PublicID: "2", Name: "Dst", Peers: []string{"peer-b", "peer-c"}}, }, - Policies: []*types.Policy{ + Policies: []*nmdata.Policy{ { ID: "pol-1", PublicID: "10", Enabled: true, - Rules: []*types.PolicyRule{{ - ID: "rule-1", Enabled: true, Action: types.PolicyTrafficActionAccept, - Protocol: types.PolicyRuleProtocolTCP, Bidirectional: true, + Rules: []*nmdata.PolicyRule{{ + ID: "rule-1", Enabled: true, Action: string(types.PolicyTrafficActionAccept), + Protocol: string(types.PolicyRuleProtocolTCP), Bidirectional: true, Ports: []string{"22", "80"}, - PortRanges: []types.RulePortRange{{Start: 8000, End: 8100}}, + PortRanges: []nmdata.RulePortRange{{Start: 8000, End: 8100}}, Sources: []string{"group-src"}, Destinations: []string{"group-dst"}, }}, }, }, - RouterPeers: map[string]*nbpeer.Peer{"peer-c": peerC}, + RouterPeers: map[string]*nmdata.Peer{"peer-c": peerC}, } } @@ -381,12 +377,12 @@ func TestEncodeNetworkMapEnvelope_MalformedWgKey(t *testing.T) { func TestEncodeNetworkMapEnvelope_IPv6OnlyPeer(t *testing.T) { c := newTestComponents() - v6Only := &nbpeer.Peer{ + v6Only := &nmdata.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"}, + Meta: nmdata.PeerSystemMeta{WtVersion: "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{ + c.Peers["peer-noip"] = &nmdata.Peer{ ID: "peer-noip", Key: testWgKeyA, DNSLabel: "peernoip", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, } full := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}).GetFull() @@ -427,7 +423,7 @@ func TestEncodeNetworkMapEnvelope_PeerWithoutIP(t *testing.T) { func TestEncodeNetworkMapEnvelope_EmptyInput(t *testing.T) { c := &types.NetworkMapComponents{ - Network: &types.Network{Identifier: "x", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}}, + Network: &nmdata.Network{Identifier: "x", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}}, } env := EncodeNetworkMapEnvelope(ComponentsEnvelopeInput{Components: c}) @@ -476,7 +472,7 @@ func TestEncodeNetworkMapEnvelope_PeerLoginExpirationFields(t *testing.T) { func TestEncodeNetworkMapEnvelope_RoutesRoundTrip(t *testing.T) { c := newTestComponents() - c.Routes = []*nbroute.Route{ + c.Routes = []*nmdata.Route{ { ID: "route-peer", PublicID: "100", @@ -523,7 +519,7 @@ func TestEncodeNetworkMapEnvelope_RoutesRoundTrip(t *testing.T) { func TestEncodeNetworkMapEnvelope_RouteWithMissingPeerLeavesIndexUnset(t *testing.T) { c := newTestComponents() - c.Routes = []*nbroute.Route{{ + c.Routes = []*nmdata.Route{{ ID: "route-x", PublicID: "100", Peer: "peer-not-in-components", @@ -543,21 +539,21 @@ func TestEncodeNetworkMapEnvelope_ResourceOnlyPolicyShippedAndIndexed(t *testing // Policy that exists ONLY in ResourcePoliciesMap, not in c.Policies. This // is the I1 case — without unionPolicies the encoder would silently // drop it from the wire. - resourceOnlyPolicy := &types.Policy{ + resourceOnlyPolicy := &nmdata.Policy{ ID: "pol-resource", PublicID: "99", Enabled: true, - Rules: []*types.PolicyRule{{ - ID: "rule-r", Enabled: true, Action: types.PolicyTrafficActionAccept, - Protocol: types.PolicyRuleProtocolTCP, + Rules: []*nmdata.PolicyRule{{ + ID: "rule-r", Enabled: true, Action: string(types.PolicyTrafficActionAccept), + Protocol: string(types.PolicyRuleProtocolTCP), Sources: []string{"group-src"}, Destinations: []string{"group-dst"}, }}, } - c.ResourcePoliciesMap = map[string][]*types.Policy{ + c.ResourcePoliciesMap = map[string][]*nmdata.Policy{ "resource-x": {c.Policies[0], resourceOnlyPolicy}, // shared + resource-only } // 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 = []*nmdata.NetworkResource{ {ID: "resource-x", PublicID: "77", Name: "res-x", Enabled: true}, } @@ -583,10 +579,10 @@ func TestEncodeNetworkMapEnvelope_ResourceOnlyPolicyShippedAndIndexed(t *testing func TestEncodeNetworkMapEnvelope_NameServerGroups(t *testing.T) { c := newTestComponents() - c.NameServerGroups = []*nbdns.NameServerGroup{{ + c.NameServerGroups = []*nmdata.NameServerGroup{{ ID: "nsg-1", PublicID: "50", Name: "Main", Description: "primary", - NameServers: []nbdns.NameServer{{ - IP: netip.MustParseAddr("8.8.8.8"), NSType: nbdns.UDPNameServerType, Port: 53, + NameServers: []nmdata.NameServer{{ + IP: netip.MustParseAddr("8.8.8.8"), NSType: int(nbdns.UDPNameServerType), Port: 53, }}, Groups: []string{"group-src", "group-not-persisted"}, Primary: true, 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]*nmdata.NetworkRouter{ "net-1": { "peer-c": { - ID: "router-1", PublicID: "200", - Peer: "peer-c", Masquerade: true, Metric: 10, Enabled: true, + PublicID: "200", + 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 := &nmdata.Peer{ 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", Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, } - c.RouterPeers = map[string]*nbpeer.Peer{"peer-c": routerPeer} + c.RouterPeers = map[string]*nmdata.Peer{"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]*nmdata.NetworkRouter{ + "net-1": {"peer-c": {PublicID: "1", 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: []*nmdata.Peer{{ 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", Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, }}, FirewallRules: []*types.FirewallRule{{ PeerIP: "100.64.0.9", Action: "accept", Direction: 0, Protocol: "tcp", @@ -766,7 +762,7 @@ func TestEncodeNetworkMapEnvelope_NilComponentsGracefulDegrade(t *testing.T) { func TestEncodeNetworkMapEnvelope_AccountSettingsAlwaysEmitted(t *testing.T) { c := &types.NetworkMapComponents{ - Network: &types.Network{Identifier: "x", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}}, + Network: &nmdata.Network{Identifier: "x", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}}, // AccountSettings deliberately nil } @@ -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]*nmdata.Peer{"peer-id": {}}}, ) } diff --git a/management/internals/shared/grpc/components_envelope_response.go b/management/internals/shared/grpc/components_envelope_response.go index cedd1b889..57c765625 100644 --- a/management/internals/shared/grpc/components_envelope_response.go +++ b/management/internals/shared/grpc/components_envelope_response.go @@ -12,6 +12,7 @@ import ( "github.com/netbirdio/netbird/management/server/types" sharedgrpc "github.com/netbirdio/netbird/shared/management/grpc" "github.com/netbirdio/netbird/shared/management/networkmap" + nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" ) @@ -170,25 +171,25 @@ func computeSSHEnabledForPeer(c *types.NetworkMapComponents, peer *nbpeer.Peer) // ruleEnablesSSHForPeer returns true when rule is active, targets peer, and // either explicitly authorises SSH or covers the legacy TCP/22 path while the // peer itself has SSH enabled locally. -func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *types.PolicyRule, peer *nbpeer.Peer) bool { +func ruleEnablesSSHForPeer(c *types.NetworkMapComponents, rule *nmdata.PolicyRule, peer *nbpeer.Peer) bool { if rule == nil || !rule.Enabled { return false } if !peerInDestinations(c, rule, peer.ID) { return false } - if rule.Protocol == types.PolicyRuleProtocolNetbirdSSH { + if rule.Protocol == string(types.PolicyRuleProtocolNetbirdSSH) { return true } - return peer.SSHEnabled && types.PolicyRuleImpliesLegacySSH(rule) + return peer.SSHEnabled && nmdata.PolicyRuleImpliesLegacySSH(rule) } // peerInDestinations reports whether peerID is in any of rule.Destinations' // groups (or matches DestinationResource if it's a peer-typed resource — // for non-peer types Calculate falls through to group lookup, so we mirror // that exactly to avoid silent divergence). -func peerInDestinations(c *types.NetworkMapComponents, rule *types.PolicyRule, peerID string) bool { - if rule.DestinationResource.Type == types.ResourceTypePeer && rule.DestinationResource.ID != "" { +func peerInDestinations(c *types.NetworkMapComponents, rule *nmdata.PolicyRule, peerID string) bool { + if rule.DestinationResource.Type == string(types.ResourceTypePeer) && rule.DestinationResource.ID != "" { return rule.DestinationResource.ID == peerID } for _, groupID := range rule.Destinations { diff --git a/management/internals/shared/grpc/components_envelope_response_test.go b/management/internals/shared/grpc/components_envelope_response_test.go index bf35bb7b9..34ef66fdf 100644 --- a/management/internals/shared/grpc/components_envelope_response_test.go +++ b/management/internals/shared/grpc/components_envelope_response_test.go @@ -7,6 +7,7 @@ import ( nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/types" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) // TestComputeSSHEnabledForPeer covers both Calculate-mirroring branches: @@ -17,16 +18,15 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { const targetPeerID = "target" const targetGroupID = "g_dst" - mkComponents := func(rule *types.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nbpeer.Peer) { + mkComponents := func(rule *nmdata.PolicyRule, sshEnabled bool) (*types.NetworkMapComponents, *nbpeer.Peer) { peer := &nbpeer.Peer{ID: targetPeerID, SSHEnabled: sshEnabled} - group := &types.Group{ID: targetGroupID, Name: "dst", Peers: []string{targetPeerID}} return &types.NetworkMapComponents{ - Peers: map[string]*nbpeer.Peer{targetPeerID: peer}, - Groups: map[string]*types.Group{targetGroupID: group}, - Policies: []*types.Policy{{ + Peers: map[string]*nmdata.Peer{targetPeerID: types.TwinPeer(peer)}, + Groups: map[string]*nmdata.Group{targetGroupID: {Name: "dst", Peers: []string{targetPeerID}}}, + Policies: []*nmdata.Policy{{ ID: "p", Enabled: true, - Rules: []*types.PolicyRule{rule}, + Rules: []*nmdata.PolicyRule{rule}, }}, }, peer } @@ -34,14 +34,14 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { cases := []struct { name string peerSSH bool - rule types.PolicyRule + rule nmdata.PolicyRule wantEnabled bool }{ { name: "explicit-netbird-ssh-activates-regardless-of-peer-ssh", peerSSH: false, - rule: types.PolicyRule{ - Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH, + rule: nmdata.PolicyRule{ + Enabled: true, Protocol: string(types.PolicyRuleProtocolNetbirdSSH), Destinations: []string{targetGroupID}, }, wantEnabled: true, @@ -49,8 +49,8 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "implicit-tcp-22-with-peer-ssh", peerSSH: true, - rule: types.PolicyRule{ - Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22"}, + rule: nmdata.PolicyRule{ + Enabled: true, Protocol: string(types.PolicyRuleProtocolTCP), Ports: []string{"22"}, Destinations: []string{targetGroupID}, }, wantEnabled: true, @@ -58,8 +58,8 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "implicit-tcp-22-without-peer-ssh-disabled", peerSSH: false, - rule: types.PolicyRule{ - Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22"}, + rule: nmdata.PolicyRule{ + Enabled: true, Protocol: string(types.PolicyRuleProtocolTCP), Ports: []string{"22"}, Destinations: []string{targetGroupID}, }, wantEnabled: false, @@ -67,8 +67,8 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "implicit-tcp-22022-with-peer-ssh", peerSSH: true, - rule: types.PolicyRule{ - Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"22022"}, + rule: nmdata.PolicyRule{ + Enabled: true, Protocol: string(types.PolicyRuleProtocolTCP), Ports: []string{"22022"}, Destinations: []string{targetGroupID}, }, wantEnabled: true, @@ -76,8 +76,8 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "implicit-all-protocol-with-peer-ssh", peerSSH: true, - rule: types.PolicyRule{ - Enabled: true, Protocol: types.PolicyRuleProtocolALL, + rule: nmdata.PolicyRule{ + Enabled: true, Protocol: string(types.PolicyRuleProtocolALL), Destinations: []string{targetGroupID}, }, wantEnabled: true, @@ -85,10 +85,10 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "implicit-port-range-covers-22", peerSSH: true, - rule: types.PolicyRule{ + rule: nmdata.PolicyRule{ Enabled: true, - Protocol: types.PolicyRuleProtocolTCP, - PortRanges: []types.RulePortRange{{Start: 20, End: 30}}, + Protocol: string(types.PolicyRuleProtocolTCP), + PortRanges: []nmdata.RulePortRange{{Start: 20, End: 30}}, Destinations: []string{targetGroupID}, }, wantEnabled: true, @@ -96,8 +96,8 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "tcp-80-no-ssh", peerSSH: true, - rule: types.PolicyRule{ - Enabled: true, Protocol: types.PolicyRuleProtocolTCP, Ports: []string{"80"}, + rule: nmdata.PolicyRule{ + Enabled: true, Protocol: string(types.PolicyRuleProtocolTCP), Ports: []string{"80"}, Destinations: []string{targetGroupID}, }, wantEnabled: false, @@ -105,8 +105,8 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "disabled-rule-skipped", peerSSH: true, - rule: types.PolicyRule{ - Enabled: false, Protocol: types.PolicyRuleProtocolNetbirdSSH, + rule: nmdata.PolicyRule{ + Enabled: false, Protocol: string(types.PolicyRuleProtocolNetbirdSSH), Destinations: []string{targetGroupID}, }, wantEnabled: false, @@ -114,8 +114,8 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "peer-not-in-destinations", peerSSH: true, - rule: types.PolicyRule{ - Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH, + rule: nmdata.PolicyRule{ + Enabled: true, Protocol: string(types.PolicyRuleProtocolNetbirdSSH), Destinations: []string{"g_other"}, // target not in this group }, wantEnabled: false, @@ -123,21 +123,21 @@ func TestComputeSSHEnabledForPeer(t *testing.T) { { name: "peer-typed-destination-resource-matches", peerSSH: false, - rule: types.PolicyRule{ + rule: nmdata.PolicyRule{ Enabled: true, - Protocol: types.PolicyRuleProtocolNetbirdSSH, - DestinationResource: types.Resource{ID: targetPeerID, Type: types.ResourceTypePeer}, + Protocol: string(types.PolicyRuleProtocolNetbirdSSH), + DestinationResource: nmdata.Resource{ID: targetPeerID, Type: string(types.ResourceTypePeer)}, }, wantEnabled: true, }, { name: "non-peer-destination-resource-falls-through-to-groups", peerSSH: false, - rule: types.PolicyRule{ + rule: nmdata.PolicyRule{ Enabled: true, - Protocol: types.PolicyRuleProtocolNetbirdSSH, - DestinationResource: types.Resource{ID: targetPeerID, Type: "host"}, // wrong type - Destinations: []string{targetGroupID}, // saved by group fallback + Protocol: string(types.PolicyRuleProtocolNetbirdSSH), + DestinationResource: nmdata.Resource{ID: targetPeerID, Type: "host"}, // wrong type + Destinations: []string{targetGroupID}, // saved by group fallback }, wantEnabled: true, }, @@ -158,14 +158,14 @@ 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{ - "g": {ID: "g", Peers: []string{"missing"}}, + Peers: map[string]*nmdata.Peer{}, // target peer NOT present + Groups: map[string]*nmdata.Group{ + "g": {Peers: []string{"missing"}}, }, - Policies: []*types.Policy{{ + Policies: []*nmdata.Policy{{ ID: "p", Enabled: true, - Rules: []*types.PolicyRule{{ - Enabled: true, Protocol: types.PolicyRuleProtocolNetbirdSSH, + Rules: []*nmdata.PolicyRule{{ + Enabled: true, Protocol: string(types.PolicyRuleProtocolNetbirdSSH), Destinations: []string{"g"}, }}, }}, diff --git a/management/internals/shared/grpc/conversion.go b/management/internals/shared/grpc/conversion.go index 696d28f5c..52b20aafa 100644 --- a/management/internals/shared/grpc/conversion.go +++ b/management/internals/shared/grpc/conversion.go @@ -22,6 +22,7 @@ import ( "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/shared/management/networkmap" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/netiputil" ) @@ -119,7 +120,7 @@ func toNetbirdConfig(config *nbconfig.Config, turnCredentials *Token, relayToken return nbConfig } -func toPeerConfig(peer *nbpeer.Peer, network *types.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool) *proto.PeerConfig { +func toPeerConfig(peer *nbpeer.Peer, network *nmdata.Network, dnsName string, settings *types.Settings, httpConfig *nbconfig.HttpServerConfig, deviceFlowConfig *nbconfig.DeviceAuthorizationFlow, enableSSH bool) *proto.PeerConfig { netmask, _ := network.Net.Mask.Size() fqdn := peer.FQDN(dnsName) diff --git a/management/internals/shared/grpc/server.go b/management/internals/shared/grpc/server.go index 3b7d62ac7..b3b381e21 100644 --- a/management/internals/shared/grpc/server.go +++ b/management/internals/shared/grpc/server.go @@ -921,7 +921,7 @@ func (s *Server) prepareLoginResponse(ctx context.Context, peer *nbpeer.Peer, ne // if peer has reached this point then it has logged in loginResp := &proto.LoginResponse{ NetbirdConfig: toNetbirdConfig(s.config, nil, relayToken, nil, settings), - PeerConfig: toPeerConfig(peer, network, s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, enableSSH), + PeerConfig: toPeerConfig(peer, types.TwinNetwork(network), s.networkMapController.GetDNSDomain(settings), settings, s.config.HttpConfig, s.config.DeviceAuthorizationFlow, enableSSH), Checks: toProtocolChecks(ctx, postureChecks), } diff --git a/management/server/http/handlers/peers/peers_handler.go b/management/server/http/handlers/peers/peers_handler.go index 310f90653..1d7dd69f5 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(account.Peers, netMap, dnsDomain)) } func (h *Handler) CreateTemporaryAccess(w http.ResponseWriter, r *http.Request) { @@ -534,14 +534,21 @@ 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 resolves the twin peers in netMap back to the full account +// peers (by ID) so the API response keeps Status/Name/OS/GeoNameID, which the +// slim netmap twins intentionally don't carry. +func toAccessiblePeers(accountPeers map[string]*nbpeer.Peer, netMap *types.NetworkMap, 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)) + appendByID := func(id string) { + if p, ok := accountPeers[id]; ok && p != nil { + accessiblePeers = append(accessiblePeers, peerToAccessiblePeer(p, dnsDomain)) + } + } + for _, p := range netMap.Peers { + appendByID(p.ID) } - for _, p := range netMap.OfflinePeers { - accessiblePeers = append(accessiblePeers, peerToAccessiblePeer(p, dnsDomain)) + appendByID(p.ID) } return accessiblePeers diff --git a/management/server/peer.go b/management/server/peer.go index 5f2f5d2a2..fd217c6df 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -21,6 +21,7 @@ import ( "github.com/netbirdio/netbird/management/server/permissions/modules" "github.com/netbirdio/netbird/management/server/permissions/operations" "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/store" @@ -1588,7 +1589,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 []*nmdata.Peer) { for _, p := range peers { if p == nil || p.ID == "" || p.ID == selfPeerID { continue diff --git a/management/server/peer_test.go b/management/server/peer_test.go index d471a1302..293144190 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -57,6 +57,7 @@ import ( "github.com/netbirdio/netbird/management/server/types" nbroute "github.com/netbirdio/netbird/route" "github.com/netbirdio/netbird/shared/management/domain" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" ) @@ -1091,22 +1092,22 @@ func TestToSyncResponse(t *testing.T) { Signature: "turn-pass", } networkMap := &types.NetworkMap{ - Network: &types.Network{Net: *ipnet, Serial: 1000}, - Peers: []*nbpeer.Peer{{ + Network: &nmdata.Network{Net: *ipnet, Serial: 1000}, + Peers: []*nmdata.Peer{{ 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: []*nmdata.Peer{{ IP: netip.MustParseAddr("192.168.1.3"), IPv6: netip.MustParseAddr("fd00::3"), Key: "peer3-key", DNSLabel: "peer3", SSHEnabled: true, SSHKey: "peer3-ssh-key"}}, - Routes: []*nbroute.Route{ + Routes: []*nmdata.Route{ { ID: "route1", Network: netip.MustParsePrefix("10.0.0.0/24"), diff --git a/management/server/route_test.go b/management/server/route_test.go index 5ae18c253..aeee64908 100644 --- a/management/server/route_test.go +++ b/management/server/route_test.go @@ -1201,7 +1201,7 @@ func TestGetNetworkMap_RouteSync(t *testing.T) { peer1Routes, err := am.GetNetworkMap(context.Background(), peer1ID) require.NoError(t, err) require.Len(t, peer1Routes.Routes, 1, "we should receive one route for peer1") - require.True(t, expectedRoute.Equal(peer1Routes.Routes[0]), "received route should be equal") + require.True(t, types.TwinRoute(expectedRoute).Equal(peer1Routes.Routes[0]), "received route should be equal") peer2Routes, err := am.GetNetworkMap(context.Background(), peer2ID) require.NoError(t, err) diff --git a/management/server/types/account.go b/management/server/types/account.go index 5aebbe9da..75083e4b1 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -18,8 +18,6 @@ import ( nbdns "github.com/netbirdio/netbird/dns" proxydomain "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" - "github.com/netbirdio/netbird/management/internals/modules/zones" - "github.com/netbirdio/netbird/management/internals/modules/zones/records" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" @@ -1798,66 +1796,3 @@ func filterZoneRecordsForPeers(peer *nbpeer.Peer, customZone nbdns.CustomZone, p return filteredRecords } - -// filterPeerAppliedZones filters account zones based on the peer's group membership -func filterPeerAppliedZones(ctx context.Context, accountZones []*zones.Zone, peerGroups LookupMap) []nbdns.CustomZone { - var customZones []nbdns.CustomZone - - if len(peerGroups) == 0 { - return customZones - } - - for _, zone := range accountZones { - if !zone.Enabled || len(zone.Records) == 0 { - continue - } - - hasAccess := false - for _, distGroupID := range zone.DistributionGroups { - if _, found := peerGroups[distGroupID]; found { - hasAccess = true - break - } - } - - if !hasAccess { - continue - } - - simpleRecords := make([]nbdns.SimpleRecord, 0, len(zone.Records)) - for _, record := range zone.Records { - var recordType int - rData := record.Content - - switch record.Type { - case records.RecordTypeA: - recordType = int(dns.TypeA) - case records.RecordTypeAAAA: - recordType = int(dns.TypeAAAA) - case records.RecordTypeCNAME: - recordType = int(dns.TypeCNAME) - rData = dns.Fqdn(record.Content) - default: - log.WithContext(ctx).Warnf("unknown DNS record type %s for record %s", record.Type, record.ID) - continue - } - - simpleRecords = append(simpleRecords, nbdns.SimpleRecord{ - Name: dns.Fqdn(record.Name), - Type: recordType, - Class: nbdns.DefaultClass, - TTL: record.TTL, - RData: rData, - }) - } - - customZones = append(customZones, nbdns.CustomZone{ - Domain: dns.Fqdn(zone.Domain), - Records: simpleRecords, - SearchDomainDisabled: !zone.EnableSearchDomain, - NonAuthoritative: true, - }) - } - - return customZones -} diff --git a/management/server/types/account_components.go b/management/server/types/account_components.go index 0205a1f55..e482702ba 100644 --- a/management/server/types/account_components.go +++ b/management/server/types/account_components.go @@ -2,18 +2,14 @@ package types import ( "context" - "slices" "time" log "github.com/sirupsen/logrus" 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" ) // GetPeerNetworkMapResult dispatches to either the legacy-NetworkMap path or @@ -94,6 +90,9 @@ func (a *Account) GetPeerNetworkMapFromComponents( return nm } +// GetPeerNetworkMapComponents builds the account's slim twin store and computes +// the peer's components on it. The calculation itself lives on +// networkmap.NetworkMapData and never touches the Account. func (a *Account) GetPeerNetworkMapComponents( ctx context.Context, peerID string, @@ -104,617 +103,6 @@ func (a *Account) GetPeerNetworkMapComponents( routers map[string]map[string]*routerTypes.NetworkRouter, groupIDToUserIDs map[string][]string, ) *NetworkMapComponents { - peer := a.Peers[peerID] - // this can never happen, things are very wrong if it did - // TODO (dmitri) maybe consider using invariants? - if peer == nil { - log.WithField("peer id", peerID).Error("NetworkMapComponents are computed for a peer missing from the account") - return EmptyNetworkMapComponents(&NetworkMapComponents{ - 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}, - }) - } - - if _, ok := validatedPeersMap[peerID]; !ok { - // Mirror legacy graceful-degrade: GetPeerNetworkMapFromComponents - // returns &NetworkMap{Network: a.Network.Copy()} when components is - // nil. Match that floor so the receiving client always sees the - // account Network identifier, not a fully-empty envelope. - return EmptyNetworkMapComponents(&NetworkMapComponents{ - 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}, - }) - } - - components := &NetworkMapComponents{ - PeerID: peerID, - Network: a.Network.Copy(), - 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), - PostureFailedPeers: make(map[string]map[string]struct{}, len(a.PostureChecks)), - RouterPeers: make(map[string]*nbpeer.Peer), - NetworkXIDToPublicID: make(map[string]string, len(a.Networks)), - PostureCheckXIDToPublicID: make(map[string]string, len(a.PostureChecks)), - } - for _, n := range a.Networks { - if n != nil { - components.NetworkXIDToPublicID[n.ID] = n.PublicID - } - } - for _, pc := range a.PostureChecks { - if pc != nil { - components.PostureCheckXIDToPublicID[pc.ID] = pc.PublicID - } - } - - components.AccountSettings = &AccountSettingsInfo{ - PeerLoginExpirationEnabled: a.Settings.PeerLoginExpirationEnabled, - PeerLoginExpiration: a.Settings.PeerLoginExpiration, - PeerInactivityExpirationEnabled: a.Settings.PeerInactivityExpirationEnabled, - PeerInactivityExpiration: a.Settings.PeerInactivityExpiration, - } - - components.DNSSettings = &a.DNSSettings - - // relevantPeers always contains the target peer (peerID) - relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := a.getPeersGroupsPoliciesRoutes(ctx, peerID, peer.SSHEnabled, validatedPeersMap, &components.PostureFailedPeers) - - if len(sshReqs.neededGroupIDs) > 0 { - components.GroupIDToUserIDs = filterGroupIDToUserIDs(groupIDToUserIDs, sshReqs.neededGroupIDs) - } - if sshReqs.needAllowedUserIDs { - components.AllowedUserIDs = a.getAllowedUserIDs() - } - - components.Peers = relevantPeers - components.Groups = relevantGroups - components.Policies = relevantPolicies - components.Routes = relevantRoutes - components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid()) - - peerGroups := a.GetPeerGroups(peerID) - components.AccountZones = filterPeerAppliedZones(ctx, accountZones, peerGroups) - components.AccountZones = append(components.AccountZones, a.SynthesizePrivateServiceZones(peerID)...) - - for _, nsGroup := range a.NameServerGroups { - if nsGroup.Enabled { - for _, gID := range nsGroup.Groups { - if _, found := relevantGroups[gID]; found { - components.NameServerGroups = append(components.NameServerGroups, nsGroup) - break - } - } - } - } - - for _, resource := range a.NetworkResources { - if !resource.Enabled { - continue - } - - policies, exists := resourcePolicies[resource.ID] - if !exists { - continue - } - - addSourcePeers := false - - networkRoutingPeers, routerExists := routers[resource.NetworkID] - if routerExists { - if _, ok := networkRoutingPeers[peerID]; ok { - addSourcePeers = true - } - } - - for _, policy := range policies { - if addSourcePeers { - var peers []string - if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" { - peers = []string{policy.Rules[0].SourceResource.ID} - } else { - peers = a.getUniquePeerIDsFromGroupsIDs(ctx, policy.SourceGroups()) - } - for _, pID := range a.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, validatedPeersMap, &components.PostureFailedPeers) { - if _, exists := components.Peers[pID]; !exists { - components.Peers[pID] = a.GetPeer(pID) - } - } - } else { - peerInSources := false - if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" { - peerInSources = policy.Rules[0].SourceResource.ID == peerID - } else { - for _, groupID := range policy.SourceGroups() { - if group := a.GetGroup(groupID); group != nil && slices.Contains(group.Peers, peerID) { - peerInSources = true - break - } - } - } - if !peerInSources { - continue - } - isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, policy.SourcePostureChecks, peerID) - if !isValid && len(pname) > 0 { - if _, ok := components.PostureFailedPeers[pname]; !ok { - components.PostureFailedPeers[pname] = make(map[string]struct{}) - } - components.PostureFailedPeers[pname][peer.ID] = struct{}{} - continue - } - addSourcePeers = true - } - - for _, rule := range policy.Rules { - for _, srcGroupID := range rule.Sources { - if g := a.Groups[srcGroupID]; g != nil { - if _, exists := components.Groups[srcGroupID]; !exists { - components.Groups[srcGroupID] = g - } - } - } - for _, dstGroupID := range rule.Destinations { - if g := a.Groups[dstGroupID]; g != nil { - if _, exists := components.Groups[dstGroupID]; !exists { - components.Groups[dstGroupID] = g - } - } - } - } - components.ResourcePoliciesMap[resource.ID] = policies - } - - // Only expose router peers and the per-network routers_map when this - // target peer actually has access to the resource (either as a router - // itself or via a policy that includes it as a source). Without this - // gate, every peer's envelope was leaking router peers of every - // 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 - for peerIDKey := range networkRoutingPeers { - if p := a.Peers[peerIDKey]; p != nil { - if _, exists := components.RouterPeers[peerIDKey]; !exists { - components.RouterPeers[peerIDKey] = p - } - if _, exists := components.Peers[peerIDKey]; !exists { - if _, validated := validatedPeersMap[peerIDKey]; validated { - components.Peers[peerIDKey] = p - } - } - } - } - components.NetworkResources = append(components.NetworkResources, resource) - } - } - - filterGroupPeers(&components.Groups, components.Peers) - filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers) - - return components -} - -type sshRequirements struct { - neededGroupIDs map[string]struct{} - needAllowedUserIDs bool -} - -func (a *Account) getPeersGroupsPoliciesRoutes( - ctx context.Context, - peerID string, - 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) - 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) - - peerGroupSet := make(map[string]struct{}, 8) - for groupID, group := range a.Groups { - if slices.Contains(group.Peers, peerID) { - relevantGroupIDs[groupID] = a.GetGroup(groupID) - peerGroupSet[groupID] = struct{}{} - } - } - - routeAccessControlGroups := make(map[string]struct{}) - for _, r := range a.Routes { - if r == nil { - continue - } - relevant := r.Peer == peerID - if !relevant { - for _, groupID := range r.PeerGroups { - if _, ok := peerGroupSet[groupID]; ok { - relevant = true - break - } - } - } - if !relevant && r.Enabled { - for _, groupID := range r.Groups { - if _, ok := peerGroupSet[groupID]; ok { - relevant = true - break - } - } - } - if !relevant { - continue - } - - for _, groupID := range r.PeerGroups { - relevantGroupIDs[groupID] = a.GetGroup(groupID) - } - for _, groupID := range r.Groups { - relevantGroupIDs[groupID] = a.GetGroup(groupID) - } - if r.Enabled { - for _, groupID := range r.AccessControlGroups { - relevantGroupIDs[groupID] = a.GetGroup(groupID) - routeAccessControlGroups[groupID] = struct{}{} - } - } - - // Include route advertisers in relevantPeerIDs. The envelope - // encoder writes route.peer_index by looking up r.Peer in the - // shipped peers list; if the advertiser is policy-isolated from - // the target peer (no rule edge between them), it would otherwise - // be omitted and the decoder would fail to resolve r.Peer, leaving - // the client without a WG tunnel target for this route. Legacy - // NetworkMap.Routes shipped the WG public key inline, so the - // equivalence path doesn't surface this — but the dependency is - // real once a client actually tries to use the route. - // Gate by validatedPeersMap so non-validated advertisers stay out - // (matches the network-resource router behaviour at the bottom of - // this loop, and the legacy invariant that only validated peers - // reach a client's view). - if r.Peer != "" { - if _, ok := validatedPeersMap[r.Peer]; ok { - if p := a.GetPeer(r.Peer); p != nil { - relevantPeerIDs[r.Peer] = p - } - } - } - for _, groupID := range r.PeerGroups { - g := a.GetGroup(groupID) - if g == nil { - continue - } - for _, pid := range g.Peers { - if _, exists := relevantPeerIDs[pid]; exists { - continue - } - if _, ok := validatedPeersMap[pid]; !ok { - continue - } - if p := a.GetPeer(pid); p != nil { - relevantPeerIDs[pid] = p - } - } - } - relevantRoutes = append(relevantRoutes, r) - } - - for _, policy := range a.Policies { - if !policy.Enabled { - continue - } - - policyRelevant := false - for _, rule := range policy.Rules { - if !rule.Enabled { - continue - } - - if len(routeAccessControlGroups) > 0 { - for _, destGroupID := range rule.Destinations { - if _, needed := routeAccessControlGroups[destGroupID]; needed { - policyRelevant = true - for _, srcGroupID := range rule.Sources { - relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID) - } - for _, dstGroupID := range rule.Destinations { - relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID) - } - break - } - } - } - - var sourcePeers, destinationPeers []string - var peerInSources, peerInDestinations bool - - if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" { - sourcePeers = []string{rule.SourceResource.ID} - if rule.SourceResource.ID == peerID { - peerInSources = true - } - } else { - sourcePeers, peerInSources = a.getPeersFromGroups(ctx, rule.Sources, peerID, policy.SourcePostureChecks, validatedPeersMap, postureFailedPeers) - } - - if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" { - destinationPeers = []string{rule.DestinationResource.ID} - if rule.DestinationResource.ID == peerID { - peerInDestinations = true - } - } else { - destinationPeers, peerInDestinations = a.getPeersFromGroups(ctx, rule.Destinations, peerID, nil, validatedPeersMap, postureFailedPeers) - } - - if peerInSources { - policyRelevant = true - for _, pid := range destinationPeers { - relevantPeerIDs[pid] = a.GetPeer(pid) - } - for _, dstGroupID := range rule.Destinations { - relevantGroupIDs[dstGroupID] = a.GetGroup(dstGroupID) - } - } - - if peerInDestinations { - policyRelevant = true - for _, pid := range sourcePeers { - relevantPeerIDs[pid] = a.GetPeer(pid) - } - for _, srcGroupID := range rule.Sources { - relevantGroupIDs[srcGroupID] = a.GetGroup(srcGroupID) - } - - if rule.Protocol == PolicyRuleProtocolNetbirdSSH { - switch { - case len(rule.AuthorizedGroups) > 0: - for groupID := range rule.AuthorizedGroups { - sshReqs.neededGroupIDs[groupID] = struct{}{} - } - case rule.AuthorizedUser != "": - default: - sshReqs.needAllowedUserIDs = true - } - } else if PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled { - sshReqs.needAllowedUserIDs = true - } - } - } - if policyRelevant { - relevantPolicies = append(relevantPolicies, policy) - } - } - - return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs -} - -func (a *Account) getPeersFromGroups(ctx context.Context, groups []string, peerID string, sourcePostureChecksIDs []string, - validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) { - peerInGroups := false - filteredPeerIDs := make([]string, 0, len(groups)) - seenPeerIds := make(map[string]struct{}, len(groups)) - - for _, gid := range groups { - group := a.GetGroup(gid) - if group == nil { - continue - } - - if group.IsGroupAll() || len(groups) == 1 { - filteredPeerIDs = make([]string, 0, len(group.Peers)) - peerInGroups = false - for _, pid := range group.Peers { - peer, ok := a.Peers[pid] - if !ok || peer == nil { - continue - } - - if _, ok := validatedPeersMap[peer.ID]; !ok { - continue - } - - isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, sourcePostureChecksIDs, peer.ID) - if !isValid && len(pname) > 0 { - if _, ok := (*postureFailedPeers)[pname]; !ok { - (*postureFailedPeers)[pname] = make(map[string]struct{}) - } - (*postureFailedPeers)[pname][peer.ID] = struct{}{} - continue - } - - if peer.ID == peerID { - peerInGroups = true - continue - } - - filteredPeerIDs = append(filteredPeerIDs, peer.ID) - } - return filteredPeerIDs, peerInGroups - } - - for _, pid := range group.Peers { - if _, seen := seenPeerIds[pid]; seen { - continue - } - seenPeerIds[pid] = struct{}{} - peer, ok := a.Peers[pid] - if !ok || peer == nil { - continue - } - - if _, ok := validatedPeersMap[peer.ID]; !ok { - continue - } - - isValid, pname := a.validatePostureChecksOnPeerGetFailed(ctx, sourcePostureChecksIDs, peer.ID) - if !isValid && len(pname) > 0 { - if _, ok := (*postureFailedPeers)[pname]; !ok { - (*postureFailedPeers)[pname] = make(map[string]struct{}) - } - (*postureFailedPeers)[pname][peer.ID] = struct{}{} - continue - } - - if peer.ID == peerID { - peerInGroups = true - continue - } - - filteredPeerIDs = append(filteredPeerIDs, peer.ID) - } - } - - return filteredPeerIDs, peerInGroups -} - -func (a *Account) validatePostureChecksOnPeerGetFailed(ctx context.Context, sourcePostureChecksID []string, peerID string) (bool, string) { - peer, ok := a.Peers[peerID] - if !ok || peer == nil { - return false, "" - } - - for _, postureChecksID := range sourcePostureChecksID { - postureChecks := a.GetPostureChecks(postureChecksID) - if postureChecks == nil { - continue - } - - for _, check := range postureChecks.GetChecks() { - isValid, _ := check.Check(ctx, *peer) - if !isValid { - return false, postureChecksID - } - } - } - return true, "" -} - -func (a *Account) getPostureValidPeersSaveFailed(inputPeers []string, postureChecksIDs []string, validatedPeersMap map[string]struct{}, postureFailedPeers *map[string]map[string]struct{}) []string { - var dest []string - for _, peerID := range inputPeers { - if _, validated := validatedPeersMap[peerID]; !validated { - continue - } - valid, pname := a.validatePostureChecksOnPeerGetFailed(context.Background(), postureChecksIDs, peerID) - if valid { - dest = append(dest, peerID) - continue - } - if _, ok := (*postureFailedPeers)[pname]; !ok { - (*postureFailedPeers)[pname] = make(map[string]struct{}) - } - (*postureFailedPeers)[pname][peerID] = struct{}{} - } - return dest -} - -// filterGroupPeers trims each group's Peers slice to only those peers that -// also appear in `peers`. Groups whose filtered list is empty are NOT -// deleted from the map — they're kept so the components wire encoder can -// still resolve seq references from routes/policies/access-control groups -// 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) { - for groupID, groupInfo := range *groups { - filteredPeers := make([]string, 0, len(groupInfo.Peers)) - for _, pid := range groupInfo.Peers { - if _, exists := peers[pid]; exists { - filteredPeers = append(filteredPeers, pid) - } - } - - if len(filteredPeers) != len(groupInfo.Peers) { - ng := groupInfo.Copy() - ng.Peers = filteredPeers - (*groups)[groupID] = ng - } - } -} - -func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*Policy, resourcePoliciesMap map[string][]*Policy, peers map[string]*nbpeer.Peer) { - if len(*postureFailedPeers) == 0 { - return - } - - referencedPostureChecks := make(map[string]struct{}) - for _, policy := range policies { - for _, checkID := range policy.SourcePostureChecks { - referencedPostureChecks[checkID] = struct{}{} - } - } - for _, resPolicies := range resourcePoliciesMap { - for _, policy := range resPolicies { - for _, checkID := range policy.SourcePostureChecks { - referencedPostureChecks[checkID] = struct{}{} - } - } - } - - for checkID, failedPeers := range *postureFailedPeers { - if _, referenced := referencedPostureChecks[checkID]; !referenced { - delete(*postureFailedPeers, checkID) - continue - } - for peerID := range failedPeers { - if _, exists := peers[peerID]; !exists { - delete(failedPeers, peerID) - } - } - if len(failedPeers) == 0 { - delete(*postureFailedPeers, checkID) - } - } -} - -func filterDNSRecordsByPeers(records []nbdns.SimpleRecord, peers map[string]*nbpeer.Peer, includeIPv6 bool) []nbdns.SimpleRecord { - if len(records) == 0 || len(peers) == 0 { - return nil - } - - // Include both v4 and v6 addresses so AAAA records (whose RData is an IPv6 - // address) are not filtered out when peers have IPv6 assigned. When the - // requesting peer doesn't have IPv6, omit v6 IPs so AAAA records get dropped. - peerIPs := make(map[string]struct{}, len(peers)*2) - for _, peer := range peers { - if peer == nil { - continue - } - peerIPs[peer.IP.String()] = struct{}{} - if includeIPv6 && peer.IPv6.IsValid() { - peerIPs[peer.IPv6.String()] = struct{}{} - } - } - - filteredRecords := make([]nbdns.SimpleRecord, 0, len(records)) - for _, record := range records { - if _, exists := peerIPs[record.RData]; exists { - filteredRecords = append(filteredRecords, record) - } - } - - return filteredRecords -} - -func filterGroupIDToUserIDs(fullMap map[string][]string, neededGroupIDs map[string]struct{}) map[string][]string { - if len(neededGroupIDs) == 0 { - return nil - } - - filtered := make(map[string][]string, len(neededGroupIDs)) - for groupID := range neededGroupIDs { - if users, ok := fullMap[groupID]; ok { - filtered[groupID] = users - } - } - return filtered + nmd := a.toNetworkMapData(accountZones, validatedPeersMap, resourcePolicies, routers, groupIDToUserIDs) + return nmd.GetPeerNetworkMapComponents(peerID, toTwinCustomZone(peersCustomZone)) } diff --git a/management/server/types/account_networkmapdata.go b/management/server/types/account_networkmapdata.go new file mode 100644 index 000000000..72e5f251b --- /dev/null +++ b/management/server/types/account_networkmapdata.go @@ -0,0 +1,515 @@ +package types + +import ( + "github.com/miekg/dns" + + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/management/internals/modules/zones" + "github.com/netbirdio/netbird/management/internals/modules/zones/records" + 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/posture" + nbroute "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/networkmap" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" +) + +// toNetworkMapData builds the slim twin store from the account once per +// account. The per-peer components calculation then runs on the twin. +func (a *Account) toNetworkMapData( + accountZones []*zones.Zone, + validatedPeersMap map[string]struct{}, + resourcePolicies map[string][]*Policy, + routers map[string]map[string]*routerTypes.NetworkRouter, + groupIDToUserIDs map[string][]string, +) *networkmap.NetworkMapData { + nmd := &networkmap.NetworkMapData{ + Peers: make(map[string]*nmdata.Peer, len(a.Peers)), + Groups: make(map[string]*nmdata.Group, len(a.Groups)), + Policies: make([]*nmdata.Policy, 0, len(a.Policies)), + Routes: make([]*nmdata.Route, 0, len(a.Routes)), + NameServerGroups: make([]*nmdata.NameServerGroup, 0, len(a.NameServerGroups)), + NetworkResources: make([]*nmdata.NetworkResource, 0, len(a.NetworkResources)), + PostureChecks: make(map[string]*nmdata.PostureChecks, len(a.PostureChecks)), + ResourcePolicies: make(map[string][]*nmdata.Policy, len(resourcePolicies)), + Routers: make(map[string]map[string]*nmdata.NetworkRouter, len(routers)), + ValidatedPeers: validatedPeersMap, + GroupIDToUserIDs: groupIDToUserIDs, + AllowedUserIDs: a.getAllowedUserIDs(), + NetworkXIDToPublicID: make(map[string]string, len(a.Networks)), + PostureCheckXIDToPublicID: make(map[string]string, len(a.PostureChecks)), + } + + if a.Network != nil { + nmd.Network = TwinNetwork(a.Network) + } + nmd.DNSSettings = &nmdata.DNSSettings{DisabledManagementGroups: a.DNSSettings.DisabledManagementGroups} + if a.Settings != nil { + nmd.AccountSettings = &nmdata.AccountSettingsInfo{ + PeerLoginExpirationEnabled: a.Settings.PeerLoginExpirationEnabled, + PeerLoginExpiration: a.Settings.PeerLoginExpiration, + PeerInactivityExpirationEnabled: a.Settings.PeerInactivityExpirationEnabled, + PeerInactivityExpiration: a.Settings.PeerInactivityExpiration, + } + } + + for id, p := range a.Peers { + nmd.Peers[id] = twinPeer(p) + } + for id, g := range a.Groups { + nmd.Groups[id] = twinGroup(g) + } + + policyCache := make(map[string]*nmdata.Policy, len(a.Policies)) + twinPol := func(p *Policy) *nmdata.Policy { + if p == nil { + return nil + } + if tp, ok := policyCache[p.ID]; ok { + return tp + } + tp := twinPolicy(p) + policyCache[p.ID] = tp + return tp + } + for _, p := range a.Policies { + nmd.Policies = append(nmd.Policies, twinPol(p)) + } + for resID, pols := range resourcePolicies { + twinPols := make([]*nmdata.Policy, 0, len(pols)) + for _, p := range pols { + twinPols = append(twinPols, twinPol(p)) + } + nmd.ResourcePolicies[resID] = twinPols + } + + for _, r := range a.Routes { + if r == nil { + continue + } + nmd.Routes = append(nmd.Routes, twinRoute(r)) + } + for _, nsg := range a.NameServerGroups { + nmd.NameServerGroups = append(nmd.NameServerGroups, twinNSG(nsg)) + } + for _, res := range a.NetworkResources { + nmd.NetworkResources = append(nmd.NetworkResources, twinNetworkResource(res)) + } + for _, pc := range a.PostureChecks { + if pc != nil { + nmd.PostureChecks[pc.ID] = twinPostureChecks(pc) + nmd.PostureCheckXIDToPublicID[pc.ID] = pc.PublicID + } + } + for _, n := range a.Networks { + if n != nil { + nmd.NetworkXIDToPublicID[n.ID] = n.PublicID + } + } + for networkID, inner := range routers { + twinInner := make(map[string]*nmdata.NetworkRouter, len(inner)) + for peerID, router := range inner { + twinInner[peerID] = twinRouter(router) + } + nmd.Routers[networkID] = twinInner + } + + nmd.AppliedZoneCandidates = buildAppliedZoneCandidates(accountZones) + nmd.PrivateServiceCandidates = a.buildPrivateServiceCandidates() + + return nmd +} + +func twinPeer(p *nbpeer.Peer) *nmdata.Peer { + if p == nil { + return nil + } + networkAddresses := make([]nmdata.NetworkAddress, 0, len(p.Meta.NetworkAddresses)) + for _, na := range p.Meta.NetworkAddresses { + networkAddresses = append(networkAddresses, nmdata.NetworkAddress{NetIP: na.NetIP}) + } + files := make([]nmdata.File, 0, len(p.Meta.Files)) + for _, f := range p.Meta.Files { + files = append(files, nmdata.File{Path: f.Path, ProcessIsRunning: f.ProcessIsRunning}) + } + return &nmdata.Peer{ + ID: p.ID, + Key: p.Key, + SSHKey: p.SSHKey, + DNSLabel: p.DNSLabel, + UserID: p.UserID, + SSHEnabled: p.SSHEnabled, + LoginExpirationEnabled: p.LoginExpirationEnabled, + LastLogin: p.LastLogin, + IP: p.IP, + IPv6: p.IPv6, + Meta: nmdata.PeerSystemMeta{ + WtVersion: p.Meta.WtVersion, + GoOS: p.Meta.GoOS, + OSVersion: p.Meta.OSVersion, + KernelVersion: p.Meta.KernelVersion, + NetworkAddresses: networkAddresses, + Files: files, + Capabilities: p.Meta.Capabilities, + Flags: nmdata.Flags{ + ServerSSHAllowed: p.Meta.Flags.ServerSSHAllowed, + DisableIPv6: p.Meta.Flags.DisableIPv6, + }, + }, + Location: nmdata.PeerLocation{ + CountryCode: p.Location.CountryCode, + CityName: p.Location.CityName, + ConnectionIP: p.Location.ConnectionIP, + }, + } +} + +// TwinPeer converts a real peer to its slim nmdata twin. Exported for the +// port-forwarding integration, which builds proxy NetworkMaps holding twins. +func TwinPeer(p *nbpeer.Peer) *nmdata.Peer { + return twinPeer(p) +} + +func twinPeers(peers []*nbpeer.Peer) []*nmdata.Peer { + out := make([]*nmdata.Peer, len(peers)) + for i, p := range peers { + out[i] = twinPeer(p) + } + return out +} + +func twinGroup(g *Group) *nmdata.Group { + if g == nil { + return nil + } + return &nmdata.Group{ + Name: g.Name, + PublicID: g.PublicID, + Peers: g.Peers, + } +} + +func twinPolicy(p *Policy) *nmdata.Policy { + if p == nil { + return nil + } + rules := make([]*nmdata.PolicyRule, 0, len(p.Rules)) + for _, r := range p.Rules { + rules = append(rules, twinRule(r)) + } + return &nmdata.Policy{ + ID: p.ID, + PublicID: p.PublicID, + Enabled: p.Enabled, + SourcePostureChecks: p.SourcePostureChecks, + Rules: rules, + } +} + +func twinRule(r *PolicyRule) *nmdata.PolicyRule { + if r == nil { + return nil + } + var portRanges []nmdata.RulePortRange + if r.PortRanges != nil { + portRanges = make([]nmdata.RulePortRange, len(r.PortRanges)) + for i, pr := range r.PortRanges { + portRanges[i] = nmdata.RulePortRange{Start: pr.Start, End: pr.End} + } + } + return &nmdata.PolicyRule{ + ID: r.ID, + PolicyID: r.PolicyID, + Enabled: r.Enabled, + Action: string(r.Action), + Protocol: string(r.Protocol), + Bidirectional: r.Bidirectional, + Sources: r.Sources, + Destinations: r.Destinations, + SourceResource: nmdata.Resource{ID: r.SourceResource.ID, Type: string(r.SourceResource.Type)}, + DestinationResource: nmdata.Resource{ID: r.DestinationResource.ID, Type: string(r.DestinationResource.Type)}, + Ports: r.Ports, + PortRanges: portRanges, + AuthorizedGroups: r.AuthorizedGroups, + AuthorizedUser: r.AuthorizedUser, + } +} + +func twinRoute(r *nbroute.Route) *nmdata.Route { + return &nmdata.Route{ + ID: string(r.ID), + AccountID: r.AccountID, + PublicID: r.PublicID, + Network: r.Network, + Domains: r.Domains, + KeepRoute: r.KeepRoute, + NetID: string(r.NetID), + Description: r.Description, + Peer: r.Peer, + PeerID: r.PeerID, + PeerGroups: r.PeerGroups, + NetworkType: int(r.NetworkType), + Masquerade: r.Masquerade, + Metric: r.Metric, + Enabled: r.Enabled, + Groups: r.Groups, + AccessControlGroups: r.AccessControlGroups, + SkipAutoApply: r.SkipAutoApply, + } +} + +// TwinRoute converts a real *route.Route to its slim nmdata twin. Exported for +// tests that assert against twin routes returned in a NetworkMap. +func TwinRoute(r *nbroute.Route) *nmdata.Route { + return twinRoute(r) +} + +func twinNetworkResource(r *resourceTypes.NetworkResource) *nmdata.NetworkResource { + if r == nil { + return nil + } + return &nmdata.NetworkResource{ + ID: r.ID, + NetworkID: r.NetworkID, + AccountID: r.AccountID, + PublicID: r.PublicID, + Name: r.Name, + Description: r.Description, + Type: string(r.Type), + Address: r.Address, + Domain: r.Domain, + Prefix: r.Prefix, + Enabled: r.Enabled, + } +} + +func twinRouter(r *routerTypes.NetworkRouter) *nmdata.NetworkRouter { + if r == nil { + return nil + } + return &nmdata.NetworkRouter{ + PublicID: r.PublicID, + PeerGroups: r.PeerGroups, + Masquerade: r.Masquerade, + Metric: r.Metric, + Enabled: r.Enabled, + } +} + +func twinNSG(n *nbdns.NameServerGroup) *nmdata.NameServerGroup { + if n == nil { + return nil + } + nameServers := make([]nmdata.NameServer, 0, len(n.NameServers)) + for _, ns := range n.NameServers { + nameServers = append(nameServers, nmdata.NameServer{ + IP: ns.IP, + NSType: int(ns.NSType), + Port: ns.Port, + }) + } + return &nmdata.NameServerGroup{ + ID: n.ID, + PublicID: n.PublicID, + Name: n.Name, + Description: n.Description, + NameServers: nameServers, + Groups: n.Groups, + Primary: n.Primary, + Domains: n.Domains, + Enabled: n.Enabled, + SearchDomainsEnabled: n.SearchDomainsEnabled, + } +} + +// TwinNetwork converts a real *Network to its slim twin. Exported for the +// graceful-degrade path that builds a minimal NetworkMapComponents directly. +func TwinNetwork(n *Network) *nmdata.Network { + nc := n.Copy() + return &nmdata.Network{ + Identifier: nc.Identifier, + Net: nc.Net, + NetV6: nc.NetV6, + Dns: nc.Dns, + Serial: nc.Serial, + } +} + +func twinPostureChecks(pc *posture.Checks) *nmdata.PostureChecks { + if pc == nil { + return nil + } + out := &nmdata.PostureChecks{ID: pc.ID} + def := pc.Checks + if def.NBVersionCheck != nil { + out.Checks.NBVersionCheck = &nmdata.NBVersionCheck{MinVersion: def.NBVersionCheck.MinVersion} + } + if def.OSVersionCheck != nil { + oc := &nmdata.OSVersionCheck{} + if def.OSVersionCheck.Android != nil { + oc.Android = &nmdata.MinVersionCheck{MinVersion: def.OSVersionCheck.Android.MinVersion} + } + if def.OSVersionCheck.Darwin != nil { + oc.Darwin = &nmdata.MinVersionCheck{MinVersion: def.OSVersionCheck.Darwin.MinVersion} + } + if def.OSVersionCheck.Ios != nil { + oc.Ios = &nmdata.MinVersionCheck{MinVersion: def.OSVersionCheck.Ios.MinVersion} + } + if def.OSVersionCheck.Linux != nil { + oc.Linux = &nmdata.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Linux.MinKernelVersion} + } + if def.OSVersionCheck.Windows != nil { + oc.Windows = &nmdata.MinKernelVersionCheck{MinKernelVersion: def.OSVersionCheck.Windows.MinKernelVersion} + } + out.Checks.OSVersionCheck = oc + } + if def.GeoLocationCheck != nil { + gc := &nmdata.GeoLocationCheck{Action: def.GeoLocationCheck.Action} + for _, loc := range def.GeoLocationCheck.Locations { + gc.Locations = append(gc.Locations, nmdata.GeoLocation{CountryCode: loc.CountryCode, CityName: loc.CityName}) + } + out.Checks.GeoLocationCheck = gc + } + if def.PeerNetworkRangeCheck != nil { + out.Checks.PeerNetworkRangeCheck = &nmdata.PeerNetworkRangeCheck{ + Action: def.PeerNetworkRangeCheck.Action, + Ranges: def.PeerNetworkRangeCheck.Ranges, + } + } + if def.ProcessCheck != nil { + procs := make([]nmdata.Process, 0, len(def.ProcessCheck.Processes)) + for _, p := range def.ProcessCheck.Processes { + procs = append(procs, nmdata.Process{LinuxPath: p.LinuxPath, MacPath: p.MacPath, WindowsPath: p.WindowsPath}) + } + out.Checks.ProcessCheck = &nmdata.ProcessCheck{Processes: procs} + } + return out +} + +// buildAppliedZoneCandidates precomputes the account-level custom DNS zones +// (record conversion) once; the per-peer distribution-group gate runs in the +// components calc. Mirrors the account-level half of filterPeerAppliedZones. +func buildAppliedZoneCandidates(accountZones []*zones.Zone) []networkmap.AppliedZoneCandidate { + var out []networkmap.AppliedZoneCandidate + for _, zone := range accountZones { + if !zone.Enabled || len(zone.Records) == 0 { + continue + } + simpleRecords := make([]nmdata.SimpleRecord, 0, len(zone.Records)) + for _, record := range zone.Records { + var recordType int + rData := record.Content + switch record.Type { + case records.RecordTypeA: + recordType = int(dns.TypeA) + case records.RecordTypeAAAA: + recordType = int(dns.TypeAAAA) + case records.RecordTypeCNAME: + recordType = int(dns.TypeCNAME) + rData = dns.Fqdn(record.Content) + default: + continue + } + simpleRecords = append(simpleRecords, nmdata.SimpleRecord{ + Name: dns.Fqdn(record.Name), + Type: recordType, + Class: nbdns.DefaultClass, + TTL: record.TTL, + RData: rData, + }) + } + out = append(out, networkmap.AppliedZoneCandidate{ + DistributionGroups: zone.DistributionGroups, + Zone: nmdata.CustomZone{ + Domain: dns.Fqdn(zone.Domain), + Records: simpleRecords, + SearchDomainDisabled: !zone.EnableSearchDomain, + NonAuthoritative: true, + }, + }) + } + return out +} + +// buildPrivateServiceCandidates precomputes the connected-proxy A records per +// private service (account-level); the per-peer access-group gate + apex merge +// run in the components calc. Mirrors the account-level half of +// SynthesizePrivateServiceZones. +func (a *Account) buildPrivateServiceCandidates() []networkmap.PrivateServiceCandidate { + if len(a.Services) == 0 { + return nil + } + proxyPeersByCluster := a.GetProxyPeers() + if len(proxyPeersByCluster) == 0 { + return nil + } + + var out []networkmap.PrivateServiceCandidate + for _, svc := range a.Services { + if svc == nil || !svc.Enabled || !svc.Private { + continue + } + if len(svc.AccessGroups) == 0 { + continue + } + proxyPeers := proxyPeersByCluster[svc.ProxyCluster] + if len(proxyPeers) == 0 { + continue + } + apex := a.privateServiceDomainZone(svc) + if apex == "" { + continue + } + + var recs []nmdata.SimpleRecord + for _, p := range proxyPeers { + if p == nil || !p.IP.IsValid() { + continue + } + if p.Status == nil || !p.Status.Connected { + continue + } + recs = append(recs, nmdata.SimpleRecord{ + Name: dns.Fqdn(svc.Domain), + Type: int(dns.TypeA), + Class: nbdns.DefaultClass, + TTL: privateServiceDNSRecordTTL, + RData: p.IP.String(), + }) + } + if len(recs) == 0 { + continue + } + + out = append(out, networkmap.PrivateServiceCandidate{ + AccessGroups: svc.AccessGroups, + Zone: nmdata.CustomZone{ + Domain: dns.Fqdn(apex), + Records: recs, + NonAuthoritative: true, + SearchDomainDisabled: true, + }, + }) + } + return out +} + +func toTwinCustomZone(z nbdns.CustomZone) nmdata.CustomZone { + records := make([]nmdata.SimpleRecord, 0, len(z.Records)) + for _, r := range z.Records { + records = append(records, nmdata.SimpleRecord{ + Name: r.Name, + Type: r.Type, + Class: r.Class, + TTL: r.TTL, + RData: r.RData, + }) + } + return nmdata.CustomZone{ + Domain: z.Domain, + Records: records, + SearchDomainDisabled: z.SearchDomainDisabled, + NonAuthoritative: z.NonAuthoritative, + } +} diff --git a/management/server/types/account_private_netmap_test.go b/management/server/types/account_private_netmap_test.go index dc097ce26..c4b7c6bc7 100644 --- a/management/server/types/account_private_netmap_test.go +++ b/management/server/types/account_private_netmap_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" nbdns "github.com/netbirdio/netbird/dns" - nbpeer "github.com/netbirdio/netbird/management/server/peer" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) func TestPrivateService_NetworkMap_UserPeer_AndProxyPeer(t *testing.T) { @@ -49,7 +49,7 @@ func TestPrivateService_NetworkMap_UserPeer_AndProxyPeer(t *testing.T) { }) } -func netmapPeerIDs(peers []*nbpeer.Peer) []string { +func netmapPeerIDs(peers []*nmdata.Peer) []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..a4a7e04ab 100644 --- a/management/server/types/account_test.go +++ b/management/server/types/account_test.go @@ -13,8 +13,6 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service" - "github.com/netbirdio/netbird/management/internals/modules/zones" - "github.com/netbirdio/netbird/management/internals/modules/zones/records" resourceTypes "github.com/netbirdio/netbird/management/server/networks/resources/types" routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types" networkTypes "github.com/netbirdio/netbird/management/server/networks/types" @@ -1040,518 +1038,6 @@ func Test_FilterZoneRecordsForPeers(t *testing.T) { } } -func Test_filterPeerAppliedZones(t *testing.T) { - ctx := context.Background() - - tests := []struct { - name string - accountZones []*zones.Zone - peerGroups LookupMap - expected []nbdns.CustomZone - }{ - { - name: "empty peer groups returns empty custom zones", - accountZones: []*zones.Zone{}, - peerGroups: LookupMap{}, - expected: []nbdns.CustomZone{}, - }, - { - name: "peer has access to zone with A record", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "example.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "www.example.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{ - { - Domain: "example.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.example.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.1", - }, - }, - SearchDomainDisabled: true, - }, - }, - }, - { - name: "peer has access to zone with search domain enabled", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "internal.local", - Enabled: true, - EnableSearchDomain: true, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "api.internal.local", - Type: records.RecordTypeA, - Content: "10.0.0.1", - TTL: 600, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{ - { - Domain: "internal.local.", - Records: []nbdns.SimpleRecord{ - { - Name: "api.internal.local.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 600, - RData: "10.0.0.1", - }, - }, - SearchDomainDisabled: false, - }, - }, - }, - { - name: "peer has no access to zone", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "private.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group2"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "secret.private.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{}, - }, - { - name: "disabled zone is filtered out", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "disabled.com", - Enabled: false, - EnableSearchDomain: false, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "www.disabled.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{}, - }, - { - name: "zone with no records is filtered out", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "empty.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{}, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{}, - }, - { - name: "peer has access via multiple groups", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "multi.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group1", "group2", "group3"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "www.multi.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - }, - }, - }, - peerGroups: LookupMap{"group2": struct{}{}}, - expected: []nbdns.CustomZone{ - { - Domain: "multi.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.multi.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.1", - }, - }, - SearchDomainDisabled: true, - }, - }, - }, - { - name: "multiple zones with mixed access", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "allowed.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "www.allowed.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - }, - }, - { - ID: "zone2", - Domain: "denied.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group2"}, - Records: []*records.Record{ - { - ID: "record2", - Name: "www.denied.com", - Type: records.RecordTypeA, - Content: "192.168.1.2", - TTL: 300, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{ - { - Domain: "allowed.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.allowed.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.1", - }, - }, - SearchDomainDisabled: true, - }, - }, - }, - { - name: "zone with multiple record types", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "mixed.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "www.mixed.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - { - ID: "record2", - Name: "ipv6.mixed.com", - Type: records.RecordTypeAAAA, - Content: "2001:db8::1", - TTL: 600, - }, - { - ID: "record3", - Name: "alias.mixed.com", - Type: records.RecordTypeCNAME, - Content: "www.mixed.com", - TTL: 900, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{ - { - Domain: "mixed.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.mixed.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.1", - }, - { - Name: "ipv6.mixed.com.", - Type: int(dns.TypeAAAA), - Class: nbdns.DefaultClass, - TTL: 600, - RData: "2001:db8::1", - }, - { - Name: "alias.mixed.com.", - Type: int(dns.TypeCNAME), - Class: nbdns.DefaultClass, - TTL: 900, - RData: "www.mixed.com.", - }, - }, - SearchDomainDisabled: true, - }, - }, - }, - { - name: "multiple zones both accessible", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "first.com", - Enabled: true, - EnableSearchDomain: true, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "www.first.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - }, - }, - { - ID: "zone2", - Domain: "second.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record2", - Name: "www.second.com", - Type: records.RecordTypeA, - Content: "192.168.1.2", - TTL: 600, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{ - { - Domain: "first.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.first.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.1", - }, - }, - SearchDomainDisabled: false, - }, - { - Domain: "second.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.second.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 600, - RData: "192.168.1.2", - }, - }, - SearchDomainDisabled: true, - }, - }, - }, - { - name: "zone with multiple records of same type", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "multi-a.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "www.multi-a.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - { - ID: "record2", - Name: "www.multi-a.com", - Type: records.RecordTypeA, - Content: "192.168.1.2", - TTL: 300, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}}, - expected: []nbdns.CustomZone{ - { - Domain: "multi-a.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.multi-a.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.1", - }, - { - Name: "www.multi-a.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.2", - }, - }, - SearchDomainDisabled: true, - }, - }, - }, - { - name: "peer in multiple groups accessing different zones", - accountZones: []*zones.Zone{ - { - ID: "zone1", - Domain: "zone1.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group1"}, - Records: []*records.Record{ - { - ID: "record1", - Name: "www.zone1.com", - Type: records.RecordTypeA, - Content: "192.168.1.1", - TTL: 300, - }, - }, - }, - { - ID: "zone2", - Domain: "zone2.com", - Enabled: true, - EnableSearchDomain: false, - DistributionGroups: []string{"group2"}, - Records: []*records.Record{ - { - ID: "record2", - Name: "www.zone2.com", - Type: records.RecordTypeA, - Content: "192.168.1.2", - TTL: 300, - }, - }, - }, - }, - peerGroups: LookupMap{"group1": struct{}{}, "group2": struct{}{}}, - expected: []nbdns.CustomZone{ - { - Domain: "zone1.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.zone1.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.1", - }, - }, - SearchDomainDisabled: true, - }, - { - Domain: "zone2.com.", - Records: []nbdns.SimpleRecord{ - { - Name: "www.zone2.com.", - Type: int(dns.TypeA), - Class: nbdns.DefaultClass, - TTL: 300, - RData: "192.168.1.2", - }, - }, - SearchDomainDisabled: true, - }, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := filterPeerAppliedZones(ctx, tt.accountZones, tt.peerGroups) - require.Equal(t, len(tt.expected), len(result), "number of custom zones should match") - - for i, expectedZone := range tt.expected { - assert.Equal(t, expectedZone.Domain, result[i].Domain, "domain should match") - assert.Equal(t, expectedZone.SearchDomainDisabled, result[i].SearchDomainDisabled, "search domain disabled flag should match") - assert.Equal(t, len(expectedZone.Records), len(result[i].Records), "number of records should match") - - for j, expectedRecord := range expectedZone.Records { - assert.Equal(t, expectedRecord.Name, result[i].Records[j].Name, "record name should match") - assert.Equal(t, expectedRecord.Type, result[i].Records[j].Type, "record type should match") - assert.Equal(t, expectedRecord.Class, result[i].Records[j].Class, "record class should match") - assert.Equal(t, expectedRecord.TTL, result[i].Records[j].TTL, "record TTL should match") - assert.Equal(t, expectedRecord.RData, result[i].Records[j].RData, "record RData should match") - } - } - }) - } -} - func TestInjectPrivateServicePolicies_ProxyPeerGetsInboundRule(t *testing.T) { ctx := context.Background() diff --git a/management/server/types/aliases.go b/management/server/types/aliases.go index f5837a343..d7053ba86 100644 --- a/management/server/types/aliases.go +++ b/management/server/types/aliases.go @@ -67,12 +67,16 @@ func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool { return sharedtypes.PolicyRuleImpliesLegacySSH(rule) } +// ExpandPortsAndRanges / AppendIPv6FirewallRule / GenerateRouteFirewallRules +// forward to the shared twin-typed helpers, converting the real types the +// legacy Account calc still uses to nmdata twins at this boundary. + func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule { - return sharedtypes.ExpandPortsAndRanges(base, rule, peer) + return sharedtypes.ExpandPortsAndRanges(base, twinRule(rule), twinPeer(peer)) } func AppendIPv6FirewallRule(rules []*FirewallRule, rulesExists map[string]struct{}, peer, targetPeer *nbpeer.Peer, rule *PolicyRule, rc FirewallRuleContext) []*FirewallRule { - return sharedtypes.AppendIPv6FirewallRule(rules, rulesExists, peer, targetPeer, rule, rc) + return sharedtypes.AppendIPv6FirewallRule(rules, rulesExists, twinPeer(peer), twinPeer(targetPeer), twinRule(rule), rc) } func CalculateNetworkMapFromComponents(ctx context.Context, components *NetworkMapComponents) *NetworkMap { @@ -80,7 +84,7 @@ func CalculateNetworkMapFromComponents(ctx context.Context, components *NetworkM } func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule { - return sharedtypes.GenerateRouteFirewallRules(ctx, route, rule, groupPeers, direction, includeIPv6) + return sharedtypes.GenerateRouteFirewallRules(ctx, twinRoute(route), twinRule(rule), twinPeers(groupPeers), direction, includeIPv6) } func AllocateIPv6Subnet(r *rand.Rand) net.IPNet { diff --git a/management/server/types/ipv6_endtoend_test.go b/management/server/types/ipv6_endtoend_test.go index ddd1f649f..76c61369e 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/shared/management/networkmap/nmdata" ) 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 *nmdata.Peer 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..f6d542609 100644 --- a/management/server/types/networkmap_components_test.go +++ b/management/server/types/networkmap_components_test.go @@ -18,6 +18,7 @@ import ( "github.com/netbirdio/netbird/management/server/posture" "github.com/netbirdio/netbird/management/server/types" "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) func networkMapFromComponents(t *testing.T, account *types.Account, peerID string, validatedPeers map[string]struct{}) *types.NetworkMap { @@ -49,7 +50,7 @@ func allPeersValidated(account *types.Account, excludePeerIDs ...string) map[str return validated } -func peerIDs(peers []*nbpeer.Peer) []string { +func peerIDs(peers []*nmdata.Peer) []string { ids := make([]string, len(peers)) for i, p := range peers { ids[i] = p.ID @@ -625,7 +626,7 @@ func TestNetworkMapComponents_DomainNetworkResource(t *testing.T) { var hasDomainRoute bool for _, r := range nm.Routes { - if r.NetworkType == route.DomainNetwork && len(r.Domains) > 0 && r.Domains[0].SafeString() == "api.example.com" { + if r.NetworkType == int(route.DomainNetwork) && len(r.Domains) > 0 && r.Domains[0].SafeString() == "api.example.com" { hasDomainRoute = true } } diff --git a/shared/management/networkmap/decode.go b/shared/management/networkmap/decode.go index c66074b4f..81b922cd8 100644 --- a/shared/management/networkmap/decode.go +++ b/shared/management/networkmap/decode.go @@ -10,12 +10,9 @@ 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" + nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/types" ) @@ -38,28 +35,28 @@ 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)), - Policies: make([]*types.Policy, 0, len(full.Policies)), - Routes: make([]*nbroute.Route, 0, len(full.Routes)), - NameServerGroups: make([]*nbdns.NameServerGroup, 0, len(full.NameserverGroups)), + Peers: make(map[string]*nmdata.Peer, len(full.Peers)), + Groups: make(map[string]*nmdata.Group, len(full.Groups)), + Policies: make([]*nmdata.Policy, 0, len(full.Policies)), + Routes: make([]*nmdata.Route, 0, len(full.Routes)), + NameServerGroups: make([]*nmdata.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), + ResourcePoliciesMap: make(map[string][]*nmdata.Policy), + RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter), + NetworkResources: make([]*nmdata.NetworkResource, 0, len(full.NetworkResources)), + RouterPeers: make(map[string]*nmdata.Peer), AllowedUserIDs: stringSliceToSet(full.AllowedUserIds), PostureFailedPeers: make(map[string]map[string]struct{}, len(full.PostureFailedPeers)), GroupIDToUserIDs: make(map[string][]string, len(full.GroupIdToUserIds)), } if full.DnsSettings != nil { - c.DNSSettings = &types.DNSSettings{ + c.DNSSettings = &nmdata.DNSSettings{ DisabledManagementGroups: full.DnsSettings.DisabledManagementGroupIds, } } else { - c.DNSSettings = &types.DNSSettings{} + c.DNSSettings = &nmdata.DNSSettings{} } // Phase 1: peers. The envelope's peers slice is index-addressed on the @@ -101,8 +98,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, log.WithField("peer idx", idx).Error("unrecognized peer idx during decoding") } } - group := &types.Group{ - ID: groupID, + group := &nmdata.Group{ PublicID: gc.Id, Peers: peerIDs, } @@ -114,7 +110,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, // Phase 3: policies (PolicyCompact = one rule per entry; current data // model is 1 rule per policy). - policyByID := make(map[string]*types.Policy, len(full.Policies)) + policyByID := make(map[string]*nmdata.Policy, len(full.Policies)) for i, pc := range full.Policies { if pc == nil { return nil, fmt.Errorf("invalid envelope: policies[%d] is nil", i) @@ -151,7 +147,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]*nmdata.NetworkRouter, len(list.Entries)) for _, entry := range list.Entries { if !entry.PeerIndexSet { continue @@ -161,11 +157,8 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, continue } peerID := peerIDByIndex[entry.PeerIndex] - inner[peerID] = &routerTypes.NetworkRouter{ - ID: "", - NetworkID: networkID, + inner[peerID] = &nmdata.NetworkRouter{ PublicID: entry.Id, - Peer: peerID, PeerGroups: entry.PeerGroupIds, Masquerade: entry.Masquerade, Metric: int(entry.Metric), @@ -184,7 +177,7 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, if len(ids.Ids) == 0 { continue } - policies := make([]*types.Policy, 0, len(ids.Ids)) + policies := make([]*nmdata.Policy, 0, len(ids.Ids)) for _, id := range ids.Ids { if p, ok := policyByID[id]; ok { policies = append(policies, p) @@ -232,11 +225,11 @@ func DecodeEnvelope(env *proto.NetworkMapEnvelope) (*types.NetworkMapComponents, return c, nil } -func decodeAccountNetwork(an *proto.AccountNetwork) *types.Network { +func decodeAccountNetwork(an *proto.AccountNetwork) *nmdata.Network { if an == nil { return nil } - n := &types.Network{ + n := &nmdata.Network{ Identifier: an.Identifier, Dns: an.Dns, Serial: an.Serial, @@ -254,17 +247,17 @@ func decodeAccountNetwork(an *proto.AccountNetwork) *types.Network { return n } -func decodeAccountSettings(as *proto.AccountSettingsCompact) *types.AccountSettingsInfo { +func decodeAccountSettings(as *proto.AccountSettingsCompact) *nmdata.AccountSettingsInfo { if as == nil { - return &types.AccountSettingsInfo{} + return &nmdata.AccountSettingsInfo{} } - return &types.AccountSettingsInfo{ + return &nmdata.AccountSettingsInfo{ PeerLoginExpirationEnabled: as.PeerLoginExpirationEnabled, PeerLoginExpiration: time.Duration(as.PeerLoginExpirationNs), } } -func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nbpeer.Peer { +func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nmdata.Peer { var caps []int32 if pc.SupportsSourcePrefixes { caps = append(caps, nbpeer.PeerCapabilitySourcePrefixes) @@ -272,17 +265,17 @@ func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nbpeer.Peer { if pc.SupportsIpv6 { caps = append(caps, nbpeer.PeerCapabilityIPv6Overlay) } - peer := &nbpeer.Peer{ + peer := &nmdata.Peer{ ID: peerID, Key: peerID, SSHKey: string(pc.SshPubKey), SSHEnabled: pc.SshEnabled, DNSLabel: pc.DnsLabel, LoginExpirationEnabled: pc.LoginExpirationEnabled, - Meta: nbpeer.PeerSystemMeta{ + Meta: nmdata.PeerSystemMeta{ WtVersion: pc.AgentVersion, Capabilities: caps, - Flags: nbpeer.Flags{ + Flags: nmdata.Flags{ ServerSSHAllowed: pc.ServerSshAllowed, }, }, @@ -315,13 +308,13 @@ func decodePeerCompact(pc *proto.PeerCompact, peerID string) *nbpeer.Peer { return peer } -func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex []string) *types.Policy { - rule := &types.PolicyRule{ +func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex []string) *nmdata.Policy { + rule := &nmdata.PolicyRule{ ID: policyID, // 1 rule per policy → reuse synthesized id PolicyID: policyID, Enabled: true, - Action: actionFromProto(pc.Action), - Protocol: protocolFromProto(pc.Protocol), + Action: string(actionFromProto(pc.Action)), + Protocol: string(protocolFromProto(pc.Protocol)), Bidirectional: pc.Bidirectional, Ports: uint32SliceToStrings(pc.Ports), PortRanges: portRangesFromProto(pc.PortRanges), @@ -332,11 +325,11 @@ func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex SourceResource: resourceFromProto(pc.SourceResource, peerIDByIndex), DestinationResource: resourceFromProto(pc.DestinationResource, peerIDByIndex), } - return &types.Policy{ + return &nmdata.Policy{ ID: policyID, PublicID: pc.Id, Enabled: true, - Rules: []*types.PolicyRule{rule}, + Rules: []*nmdata.PolicyRule{rule}, SourcePostureChecks: pc.SourcePostureCheckIds, } } @@ -344,11 +337,11 @@ func decodePolicyCompact(pc *proto.PolicyCompact, policyID string, peerIDByIndex // resourceFromProto rebuilds types.Resource. For peer-typed resources the // peer reference is reconstructed from the envelope's peer index — wire // format ships no xid for peers, so we use the synthesized peer id. -func resourceFromProto(r *proto.ResourceCompact, peerIDByIndex []string) types.Resource { +func resourceFromProto(r *proto.ResourceCompact, peerIDByIndex []string) nmdata.Resource { if r == nil { - return types.Resource{} + return nmdata.Resource{} } - out := types.Resource{Type: types.ResourceType(r.Type)} + out := nmdata.Resource{Type: r.Type} if r.PeerIndexSet && int(r.PeerIndex) < len(peerIDByIndex) { out.ID = peerIDByIndex[r.PeerIndex] } @@ -373,15 +366,15 @@ func authorizedGroupsFromProto(m map[string]*proto.UserNameList) map[string][]st return out } -func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nbroute.Route { - r := &nbroute.Route{ - ID: nbroute.ID(rr.Id), +func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nmdata.Route { + r := &nmdata.Route{ + ID: rr.Id, PublicID: rr.Id, - NetID: nbroute.NetID(rr.NetId), + NetID: rr.NetId, Description: rr.Description, Domains: domainsFromPunycode(rr.Domains), KeepRoute: rr.KeepRoute, - NetworkType: nbroute.NetworkType(rr.NetworkType), + NetworkType: int(rr.NetworkType), Masquerade: rr.Masquerade, Metric: int(rr.Metric), Enabled: rr.Enabled, @@ -401,8 +394,8 @@ func decodeRouteRaw(rr *proto.RouteRaw, peerIDByIndex []string) *nbroute.Route { return r } -func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGroup { - out := &nbdns.NameServerGroup{ +func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nmdata.NameServerGroup { + out := &nmdata.NameServerGroup{ ID: nsg.Id, PublicID: nsg.Id, Groups: nsg.GroupIds, @@ -410,13 +403,13 @@ func decodeNameServerGroupRaw(nsg *proto.NameServerGroupRaw) *nbdns.NameServerGr Domains: nsg.Domains, Enabled: nsg.Enabled, SearchDomainsEnabled: nsg.SearchDomainsEnabled, - NameServers: make([]nbdns.NameServer, 0, len(nsg.Nameservers)), + NameServers: make([]nmdata.NameServer, 0, len(nsg.Nameservers)), } for _, ns := range nsg.Nameservers { if addr, err := netip.ParseAddr(ns.IP); err == nil { - out.NameServers = append(out.NameServers, nbdns.NameServer{ + out.NameServers = append(out.NameServers, nmdata.NameServer{ IP: addr, - NSType: nbdns.NameServerType(ns.NSType), + NSType: int(ns.NSType), Port: int(ns.Port), }) } @@ -424,14 +417,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) *nmdata.NetworkResource { + out := &nmdata.NetworkResource{ ID: nr.Id, PublicID: nr.Id, NetworkID: nr.NetworkSeq, Name: nr.Name, Description: nr.Description, - Type: resourceTypes.NetworkResourceType(nr.Type), + Type: nr.Type, Address: nr.Address, Domain: nr.DomainValue, Enabled: nr.Enabled, @@ -444,10 +437,10 @@ func decodeNetworkResource(nr *proto.NetworkResourceRaw) *resourceTypes.NetworkR return out } -func decodeSimpleRecords(records []*proto.SimpleRecord) []nbdns.SimpleRecord { - out := make([]nbdns.SimpleRecord, 0, len(records)) +func decodeSimpleRecords(records []*proto.SimpleRecord) []nmdata.SimpleRecord { + out := make([]nmdata.SimpleRecord, 0, len(records)) for _, r := range records { - out = append(out, nbdns.SimpleRecord{ + out = append(out, nmdata.SimpleRecord{ Name: r.Name, Type: int(r.Type), Class: r.Class, @@ -458,10 +451,10 @@ func decodeSimpleRecords(records []*proto.SimpleRecord) []nbdns.SimpleRecord { return out } -func decodeCustomZones(zones []*proto.CustomZone) []nbdns.CustomZone { - out := make([]nbdns.CustomZone, 0, len(zones)) +func decodeCustomZones(zones []*proto.CustomZone) []nmdata.CustomZone { + out := make([]nmdata.CustomZone, 0, len(zones)) for _, z := range zones { - out = append(out, nbdns.CustomZone{ + out = append(out, nmdata.CustomZone{ Domain: z.Domain, Records: decodeSimpleRecords(z.Records), SearchDomainDisabled: z.SearchDomainDisabled, @@ -482,16 +475,16 @@ func uint32SliceToStrings(ports []uint32) []string { return out } -func portRangesFromProto(ranges []*proto.PortInfo_Range) []types.RulePortRange { +func portRangesFromProto(ranges []*proto.PortInfo_Range) []nmdata.RulePortRange { if len(ranges) == 0 { return nil } - out := make([]types.RulePortRange, 0, len(ranges)) + out := make([]nmdata.RulePortRange, 0, len(ranges)) for _, r := range ranges { if r == nil || r.Start > 65535 || r.End > 65535 { continue } - out = append(out, types.RulePortRange{ + out = append(out, nmdata.RulePortRange{ Start: uint16(r.Start), End: uint16(r.End), }) diff --git a/shared/management/networkmap/encode.go b/shared/management/networkmap/encode.go index 0d57d66e3..30396778f 100644 --- a/shared/management/networkmap/encode.go +++ b/shared/management/networkmap/encode.go @@ -20,8 +20,7 @@ import ( nbdns "github.com/netbirdio/netbird/dns" "net/netip" - nbpeer "github.com/netbirdio/netbird/management/server/peer" - nbroute "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/types" "github.com/netbirdio/netbird/shared/netiputil" @@ -29,7 +28,7 @@ import ( ) // ToProtocolRoutes converts a slice of typed routes to their proto form. -func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route { +func ToProtocolRoutes(routes []*nmdata.Route) []*proto.Route { protoRoutes := make([]*proto.Route, 0, len(routes)) for _, r := range routes { protoRoutes = append(protoRoutes, ToProtocolRoute(r)) @@ -38,7 +37,7 @@ func ToProtocolRoutes(routes []*nbroute.Route) []*proto.Route { } // ToProtocolRoute converts one typed route to its proto form. -func ToProtocolRoute(route *nbroute.Route) *proto.Route { +func ToProtocolRoute(route *nmdata.Route) *proto.Route { return &proto.Route{ ID: string(route.ID), NetID: string(route.NetID), @@ -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 []*nmdata.Peer, dnsName string, includeIPv6 bool) []*proto.RemotePeerConfig { for _, rPeer := range peers { allowedIPs := []string{rPeer.IP.String() + "/32"} if includeIPv6 && rPeer.IPv6.IsValid() { diff --git a/shared/management/networkmap/envelope_test.go b/shared/management/networkmap/envelope_test.go index 11a5335be..77e07756c 100644 --- a/shared/management/networkmap/envelope_test.go +++ b/shared/management/networkmap/envelope_test.go @@ -13,9 +13,9 @@ 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/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" ) @@ -56,13 +56,13 @@ func TestEnvelopeToNetworkMap_RoundTrip(t *testing.T) { func TestCalculate_FirewallRuleProtocol_NeverNetbirdSSH(t *testing.T) { c, localPeerKey := buildSmokeComponents(t) // Replace the smoke policy with a NetbirdSSH-protocol allow. - c.Policies = []*types.Policy{{ + c.Policies = []*nmdata.Policy{{ ID: "pol-ssh", PublicID: "2", Enabled: true, - Rules: []*types.PolicyRule{{ + Rules: []*nmdata.PolicyRule{{ ID: "rule-ssh", Enabled: true, - Action: types.PolicyTrafficActionAccept, - Protocol: types.PolicyRuleProtocolNetbirdSSH, + Action: string(types.PolicyTrafficActionAccept), + Protocol: string(types.PolicyRuleProtocolNetbirdSSH), Bidirectional: true, Sources: []string{"group-all"}, Destinations: []string{"group-all"}, @@ -144,39 +144,39 @@ func TestDecodeEnvelope_MalformedWgKeyPeerSkipped(t *testing.T) { func TestEnvelopeRoundTrip_AllGroupShortCircuitParity(t *testing.T) { ctx := context.Background() - peers := map[string]*nbpeer.Peer{} + peers := map[string]*nmdata.Peer{} for i, id := range []string{"peer-T", "peer-S", "peer-ALL", "peer-O"} { - peers[id] = &nbpeer.Peer{ + peers[id] = &nmdata.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"}, + Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, } } c := &types.NetworkMapComponents{ PeerID: "peer-T", - Network: &types.Network{ + Network: &nmdata.Network{ Identifier: "net-all-groups", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}, Serial: 1, }, - AccountSettings: &types.AccountSettingsInfo{}, - DNSSettings: &types.DNSSettings{}, + AccountSettings: &nmdata.AccountSettingsInfo{}, + DNSSettings: &nmdata.DNSSettings{}, Peers: peers, - Groups: map[string]*types.Group{ - "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"}}, + Groups: map[string]*nmdata.Group{ + "g-src": {PublicID: "1", Name: "staff", Peers: []string{"peer-T", "peer-S"}}, + "g-all": {PublicID: "2", Name: "All", Peers: []string{"peer-ALL"}}, + "g-two": {PublicID: "3", Name: "second", Peers: []string{"peer-T", "peer-O"}}, }, - Policies: []*types.Policy{{ + Policies: []*nmdata.Policy{{ ID: "pol-multi-dest", PublicID: "10", Enabled: true, - Rules: []*types.PolicyRule{{ + Rules: []*nmdata.PolicyRule{{ ID: "rule-multi-dest", Enabled: true, - Action: types.PolicyTrafficActionAccept, - Protocol: types.PolicyRuleProtocolALL, + Action: string(types.PolicyTrafficActionAccept), + Protocol: string(types.PolicyRuleProtocolALL), Sources: []string{"g-src"}, Destinations: []string{"g-all", "g-two"}, }}, @@ -232,33 +232,33 @@ func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) { peerAKey := randomWgKey(t) peerBKey := randomWgKey(t) - peerA := &nbpeer.Peer{ + peerA := &nmdata.Peer{ ID: "peer-A", Key: peerAKey, IP: netip.AddrFrom4([4]byte{100, 64, 0, 1}), DNSLabel: "peerA", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, } - peerB := &nbpeer.Peer{ + peerB := &nmdata.Peer{ ID: "peer-B", Key: peerBKey, IP: netip.AddrFrom4([4]byte{100, 64, 0, 2}), DNSLabel: "peerB", - Meta: nbpeer.PeerSystemMeta{WtVersion: "0.40.0"}, + Meta: nmdata.PeerSystemMeta{WtVersion: "0.40.0"}, } - group := &types.Group{ - ID: "group-all", PublicID: "1", Name: "All", + group := &nmdata.Group{ + PublicID: "1", Name: "All", Peers: []string{"peer-A", "peer-B"}, } - policy := &types.Policy{ + policy := &nmdata.Policy{ ID: "pol-allow", PublicID: "1", Enabled: true, - Rules: []*types.PolicyRule{{ + Rules: []*nmdata.PolicyRule{{ ID: "rule-allow", Enabled: true, - Action: types.PolicyTrafficActionAccept, - Protocol: types.PolicyRuleProtocolALL, + Action: string(types.PolicyTrafficActionAccept), + Protocol: string(types.PolicyRuleProtocolALL), Bidirectional: true, Sources: []string{"group-all"}, Destinations: []string{"group-all"}, @@ -267,21 +267,21 @@ func buildSmokeComponents(t *testing.T) (*types.NetworkMapComponents, string) { c := &types.NetworkMapComponents{ PeerID: "peer-A", - Network: &types.Network{ + Network: &nmdata.Network{ Identifier: "net-smoke", Net: net.IPNet{IP: net.IP{100, 64, 0, 0}, Mask: net.CIDRMask(10, 32)}, Serial: 1, }, - AccountSettings: &types.AccountSettingsInfo{}, - DNSSettings: &types.DNSSettings{}, - Peers: map[string]*nbpeer.Peer{ + AccountSettings: &nmdata.AccountSettingsInfo{}, + DNSSettings: &nmdata.DNSSettings{}, + Peers: map[string]*nmdata.Peer{ "peer-A": peerA, "peer-B": peerB, }, - Groups: map[string]*types.Group{ + Groups: map[string]*nmdata.Group{ "group-all": group, }, - Policies: []*types.Policy{policy}, + Policies: []*nmdata.Policy{policy}, } return c, peerAKey } diff --git a/shared/management/networkmap/networkmapcompute.go b/shared/management/networkmap/networkmapcompute.go new file mode 100644 index 000000000..f697305a4 --- /dev/null +++ b/shared/management/networkmap/networkmapcompute.go @@ -0,0 +1,659 @@ +package networkmap + +import ( + "slices" + + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" + "github.com/netbirdio/netbird/shared/management/types" +) + +type sshRequirements struct { + neededGroupIDs map[string]struct{} + needAllowedUserIDs bool +} + +// GetPeerNetworkMapComponents computes the peer's NetworkMapComponents from the +// slim twin store. It mirrors the former Account.GetPeerNetworkMapComponents +// exactly, operating on nmdata twins throughout — no Account reference and no +// twin↔real conversion, since the produced components hold twins. +func (nmd *NetworkMapData) GetPeerNetworkMapComponents(peerID string, peersCustomZone nmdata.CustomZone) *types.NetworkMapComponents { + peer := nmd.Peers[peerID] + if peer == nil { + return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{ + PeerID: peerID, + Network: nmd.Network, + Peers: map[string]*nmdata.Peer{peerID: peer}, + }) + } + + if _, ok := nmd.ValidatedPeers[peerID]; !ok { + return types.EmptyNetworkMapComponents(&types.NetworkMapComponents{ + PeerID: peerID, + Network: nmd.Network, + Peers: map[string]*nmdata.Peer{peerID: peer}, + }) + } + + components := &types.NetworkMapComponents{ + PeerID: peerID, + Network: nmd.Network, + AccountSettings: nmd.AccountSettings, + DNSSettings: nmd.DNSSettings, + CustomZoneDomain: peersCustomZone.Domain, + NameServerGroups: make([]*nmdata.NameServerGroup, 0), + ResourcePoliciesMap: make(map[string][]*nmdata.Policy), + RoutersMap: make(map[string]map[string]*nmdata.NetworkRouter), + NetworkResources: make([]*nmdata.NetworkResource, 0), + PostureFailedPeers: make(map[string]map[string]struct{}, len(nmd.PostureChecks)), + RouterPeers: make(map[string]*nmdata.Peer), + NetworkXIDToPublicID: nmd.NetworkXIDToPublicID, + PostureCheckXIDToPublicID: nmd.PostureCheckXIDToPublicID, + } + + relevantPeers, relevantGroups, relevantPolicies, relevantRoutes, sshReqs := nmd.getPeersGroupsPoliciesRoutes(peerID, peer.SSHEnabled, &components.PostureFailedPeers) + + if len(sshReqs.neededGroupIDs) > 0 { + components.GroupIDToUserIDs = filterGroupIDToUserIDs(nmd.GroupIDToUserIDs, sshReqs.neededGroupIDs) + } + if sshReqs.needAllowedUserIDs { + components.AllowedUserIDs = nmd.getAllowedUserIDs() + } + + components.Peers = relevantPeers + components.Groups = relevantGroups + components.Policies = relevantPolicies + components.Routes = relevantRoutes + components.AllDNSRecords = filterDNSRecordsByPeers(peersCustomZone.Records, relevantPeers, peer.SupportsIPv6() && peer.IPv6.IsValid()) + + peerGroups := nmd.getPeerGroups(peerID) + components.AccountZones = nmd.appliedZones(peerGroups) + components.AccountZones = append(components.AccountZones, nmd.privateServiceZones(peerGroups)...) + + for _, nsGroup := range nmd.NameServerGroups { + if nsGroup.Enabled { + for _, gID := range nsGroup.Groups { + if _, found := relevantGroups[gID]; found { + components.NameServerGroups = append(components.NameServerGroups, nsGroup) + break + } + } + } + } + + for _, resource := range nmd.NetworkResources { + if !resource.Enabled { + continue + } + + policies, exists := nmd.ResourcePolicies[resource.ID] + if !exists { + continue + } + + addSourcePeers := false + + networkRoutingPeers, routerExists := nmd.Routers[resource.NetworkID] + if routerExists { + if _, ok := networkRoutingPeers[peerID]; ok { + addSourcePeers = true + } + } + + for _, policy := range policies { + if addSourcePeers { + var peers []string + if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" { + peers = []string{policy.Rules[0].SourceResource.ID} + } else { + peers = nmd.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups()) + } + for _, pID := range nmd.getPostureValidPeersSaveFailed(peers, policy.SourcePostureChecks, &components.PostureFailedPeers) { + if _, exists := components.Peers[pID]; !exists { + components.Peers[pID] = nmd.Peers[pID] + } + } + } else { + peerInSources := false + if policy.Rules[0].SourceResource.Type == string(types.ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" { + peerInSources = policy.Rules[0].SourceResource.ID == peerID + } else { + for _, groupID := range policy.SourceGroups() { + if group := nmd.Groups[groupID]; group != nil && slices.Contains(group.Peers, peerID) { + peerInSources = true + break + } + } + } + if !peerInSources { + continue + } + isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(policy.SourcePostureChecks, peerID) + if !isValid && len(pname) > 0 { + if _, ok := components.PostureFailedPeers[pname]; !ok { + components.PostureFailedPeers[pname] = make(map[string]struct{}) + } + components.PostureFailedPeers[pname][peer.ID] = struct{}{} + continue + } + addSourcePeers = true + } + + for _, rule := range policy.Rules { + for _, srcGroupID := range rule.Sources { + if g := nmd.Groups[srcGroupID]; g != nil { + if _, exists := components.Groups[srcGroupID]; !exists { + components.Groups[srcGroupID] = g + } + } + } + for _, dstGroupID := range rule.Destinations { + if g := nmd.Groups[dstGroupID]; g != nil { + if _, exists := components.Groups[dstGroupID]; !exists { + components.Groups[dstGroupID] = g + } + } + } + } + components.ResourcePoliciesMap[resource.ID] = policies + } + + if addSourcePeers { + components.RoutersMap[resource.NetworkID] = networkRoutingPeers + for peerIDKey := range networkRoutingPeers { + if p := nmd.Peers[peerIDKey]; p != nil { + if _, exists := components.RouterPeers[peerIDKey]; !exists { + components.RouterPeers[peerIDKey] = p + } + if _, exists := components.Peers[peerIDKey]; !exists { + if _, validated := nmd.ValidatedPeers[peerIDKey]; validated { + components.Peers[peerIDKey] = p + } + } + } + } + components.NetworkResources = append(components.NetworkResources, resource) + } + } + + filterGroupPeers(&components.Groups, components.Peers) + filterPostureFailedPeers(&components.PostureFailedPeers, components.Policies, components.ResourcePoliciesMap, components.Peers) + + return components +} + +func (nmd *NetworkMapData) getPeersGroupsPoliciesRoutes( + peerID string, + peerSSHEnabled bool, + postureFailedPeers *map[string]map[string]struct{}, +) (map[string]*nmdata.Peer, map[string]*nmdata.Group, []*nmdata.Policy, []*nmdata.Route, sshRequirements) { + relevantPeerIDs := make(map[string]*nmdata.Peer, len(nmd.Peers)/4) + relevantGroupIDs := make(map[string]*nmdata.Group, len(nmd.Groups)/4) + relevantPolicies := make([]*nmdata.Policy, 0, len(nmd.Policies)) + relevantRoutes := make([]*nmdata.Route, 0, len(nmd.Routes)) + sshReqs := sshRequirements{neededGroupIDs: make(map[string]struct{})} + + relevantPeerIDs[peerID] = nmd.Peers[peerID] + + peerGroupSet := make(map[string]struct{}, 8) + for groupID, group := range nmd.Groups { + if slices.Contains(group.Peers, peerID) { + relevantGroupIDs[groupID] = group + peerGroupSet[groupID] = struct{}{} + } + } + + routeAccessControlGroups := make(map[string]struct{}) + for _, r := range nmd.Routes { + if r == nil { + continue + } + relevant := r.Peer == peerID + if !relevant { + for _, groupID := range r.PeerGroups { + if _, ok := peerGroupSet[groupID]; ok { + relevant = true + break + } + } + } + if !relevant && r.Enabled { + for _, groupID := range r.Groups { + if _, ok := peerGroupSet[groupID]; ok { + relevant = true + break + } + } + } + if !relevant { + continue + } + + for _, groupID := range r.PeerGroups { + relevantGroupIDs[groupID] = nmd.Groups[groupID] + } + for _, groupID := range r.Groups { + relevantGroupIDs[groupID] = nmd.Groups[groupID] + } + if r.Enabled { + for _, groupID := range r.AccessControlGroups { + relevantGroupIDs[groupID] = nmd.Groups[groupID] + routeAccessControlGroups[groupID] = struct{}{} + } + } + + if r.Peer != "" { + if _, ok := nmd.ValidatedPeers[r.Peer]; ok { + if p := nmd.Peers[r.Peer]; p != nil { + relevantPeerIDs[r.Peer] = p + } + } + } + for _, groupID := range r.PeerGroups { + g := nmd.Groups[groupID] + if g == nil { + continue + } + for _, pid := range g.Peers { + if _, exists := relevantPeerIDs[pid]; exists { + continue + } + if _, ok := nmd.ValidatedPeers[pid]; !ok { + continue + } + if p := nmd.Peers[pid]; p != nil { + relevantPeerIDs[pid] = p + } + } + } + relevantRoutes = append(relevantRoutes, r) + } + + for _, policy := range nmd.Policies { + if !policy.Enabled { + continue + } + + policyRelevant := false + for _, rule := range policy.Rules { + if !rule.Enabled { + continue + } + + if len(routeAccessControlGroups) > 0 { + for _, destGroupID := range rule.Destinations { + if _, needed := routeAccessControlGroups[destGroupID]; needed { + policyRelevant = true + for _, srcGroupID := range rule.Sources { + relevantGroupIDs[srcGroupID] = nmd.Groups[srcGroupID] + } + for _, dstGroupID := range rule.Destinations { + relevantGroupIDs[dstGroupID] = nmd.Groups[dstGroupID] + } + break + } + } + } + + var sourcePeers, destinationPeers []string + var peerInSources, peerInDestinations bool + + if rule.SourceResource.Type == string(types.ResourceTypePeer) && rule.SourceResource.ID != "" { + sourcePeers = []string{rule.SourceResource.ID} + if rule.SourceResource.ID == peerID { + peerInSources = true + } + } else { + sourcePeers, peerInSources = nmd.getPeersFromGroups(rule.Sources, peerID, policy.SourcePostureChecks, postureFailedPeers) + } + + if rule.DestinationResource.Type == string(types.ResourceTypePeer) && rule.DestinationResource.ID != "" { + destinationPeers = []string{rule.DestinationResource.ID} + if rule.DestinationResource.ID == peerID { + peerInDestinations = true + } + } else { + destinationPeers, peerInDestinations = nmd.getPeersFromGroups(rule.Destinations, peerID, nil, postureFailedPeers) + } + + if peerInSources { + policyRelevant = true + for _, pid := range destinationPeers { + relevantPeerIDs[pid] = nmd.Peers[pid] + } + for _, dstGroupID := range rule.Destinations { + relevantGroupIDs[dstGroupID] = nmd.Groups[dstGroupID] + } + } + + if peerInDestinations { + policyRelevant = true + for _, pid := range sourcePeers { + relevantPeerIDs[pid] = nmd.Peers[pid] + } + for _, srcGroupID := range rule.Sources { + relevantGroupIDs[srcGroupID] = nmd.Groups[srcGroupID] + } + + if rule.Protocol == string(types.PolicyRuleProtocolNetbirdSSH) { + switch { + case len(rule.AuthorizedGroups) > 0: + for groupID := range rule.AuthorizedGroups { + sshReqs.neededGroupIDs[groupID] = struct{}{} + } + case rule.AuthorizedUser != "": + default: + sshReqs.needAllowedUserIDs = true + } + } else if nmdata.PolicyRuleImpliesLegacySSH(rule) && peerSSHEnabled { + sshReqs.needAllowedUserIDs = true + } + } + } + if policyRelevant { + relevantPolicies = append(relevantPolicies, policy) + } + } + + return relevantPeerIDs, relevantGroupIDs, relevantPolicies, relevantRoutes, sshReqs +} + +func (nmd *NetworkMapData) getPeersFromGroups(groups []string, peerID string, sourcePostureChecksIDs []string, + postureFailedPeers *map[string]map[string]struct{}) ([]string, bool) { + peerInGroups := false + filteredPeerIDs := make([]string, 0, len(groups)) + seenPeerIds := make(map[string]struct{}, len(groups)) + + for _, gid := range groups { + group := nmd.Groups[gid] + if group == nil { + continue + } + + if group.IsGroupAll() || len(groups) == 1 { + filteredPeerIDs = make([]string, 0, len(group.Peers)) + peerInGroups = false + for _, pid := range group.Peers { + peer, ok := nmd.Peers[pid] + if !ok || peer == nil { + continue + } + + if _, ok := nmd.ValidatedPeers[peer.ID]; !ok { + continue + } + + isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, peer.ID) + if !isValid && len(pname) > 0 { + if _, ok := (*postureFailedPeers)[pname]; !ok { + (*postureFailedPeers)[pname] = make(map[string]struct{}) + } + (*postureFailedPeers)[pname][peer.ID] = struct{}{} + continue + } + + if peer.ID == peerID { + peerInGroups = true + continue + } + + filteredPeerIDs = append(filteredPeerIDs, peer.ID) + } + return filteredPeerIDs, peerInGroups + } + + for _, pid := range group.Peers { + if _, seen := seenPeerIds[pid]; seen { + continue + } + seenPeerIds[pid] = struct{}{} + peer, ok := nmd.Peers[pid] + if !ok || peer == nil { + continue + } + + if _, ok := nmd.ValidatedPeers[peer.ID]; !ok { + continue + } + + isValid, pname := nmd.validatePostureChecksOnPeerGetFailed(sourcePostureChecksIDs, peer.ID) + if !isValid && len(pname) > 0 { + if _, ok := (*postureFailedPeers)[pname]; !ok { + (*postureFailedPeers)[pname] = make(map[string]struct{}) + } + (*postureFailedPeers)[pname][peer.ID] = struct{}{} + continue + } + + if peer.ID == peerID { + peerInGroups = true + continue + } + + filteredPeerIDs = append(filteredPeerIDs, peer.ID) + } + } + + return filteredPeerIDs, peerInGroups +} + +func (nmd *NetworkMapData) validatePostureChecksOnPeerGetFailed(sourcePostureChecksID []string, peerID string) (bool, string) { + peer, ok := nmd.Peers[peerID] + if !ok || peer == nil { + return false, "" + } + + for _, postureChecksID := range sourcePostureChecksID { + postureChecks := nmd.PostureChecks[postureChecksID] + if postureChecks == nil { + continue + } + if !postureChecks.Passes(peer) { + return false, postureChecksID + } + } + return true, "" +} + +func (nmd *NetworkMapData) getPostureValidPeersSaveFailed(inputPeers []string, postureChecksIDs []string, postureFailedPeers *map[string]map[string]struct{}) []string { + var dest []string + for _, peerID := range inputPeers { + if _, validated := nmd.ValidatedPeers[peerID]; !validated { + continue + } + valid, pname := nmd.validatePostureChecksOnPeerGetFailed(postureChecksIDs, peerID) + if valid { + dest = append(dest, peerID) + continue + } + if _, ok := (*postureFailedPeers)[pname]; !ok { + (*postureFailedPeers)[pname] = make(map[string]struct{}) + } + (*postureFailedPeers)[pname][peerID] = struct{}{} + } + return dest +} + +func (nmd *NetworkMapData) getPeerGroups(peerID string) map[string]struct{} { + groups := make(map[string]struct{}) + for groupID, group := range nmd.Groups { + if slices.Contains(group.Peers, peerID) { + groups[groupID] = struct{}{} + } + } + return groups +} + +func (nmd *NetworkMapData) getUniquePeerIDsFromGroupsIDs(groups []string) []string { + peerIDs := make(map[string]struct{}, len(groups)) + for _, groupID := range groups { + group := nmd.Groups[groupID] + if group == nil { + continue + } + + if group.IsGroupAll() || len(groups) == 1 { + return group.Peers + } + + for _, peerID := range group.Peers { + peerIDs[peerID] = struct{}{} + } + } + + ids := make([]string, 0, len(peerIDs)) + for peerID := range peerIDs { + ids = append(ids, peerID) + } + + return ids +} + +func (nmd *NetworkMapData) getAllowedUserIDs() map[string]struct{} { + return nmd.AllowedUserIDs +} + +func (nmd *NetworkMapData) appliedZones(peerGroups map[string]struct{}) []nmdata.CustomZone { + if len(peerGroups) == 0 { + return nil + } + var out []nmdata.CustomZone + for _, cand := range nmd.AppliedZoneCandidates { + if peerInDistributionGroups(peerGroups, cand.DistributionGroups) { + out = append(out, cand.Zone) + } + } + return out +} + +func (nmd *NetworkMapData) privateServiceZones(peerGroups map[string]struct{}) []nmdata.CustomZone { + byApex := make(map[string]*nmdata.CustomZone) + var order []string + for _, cand := range nmd.PrivateServiceCandidates { + if !peerInDistributionGroups(peerGroups, cand.AccessGroups) { + continue + } + zone, exists := byApex[cand.Zone.Domain] + if !exists { + nz := nmdata.CustomZone{ + Domain: cand.Zone.Domain, + SearchDomainDisabled: cand.Zone.SearchDomainDisabled, + NonAuthoritative: cand.Zone.NonAuthoritative, + } + byApex[cand.Zone.Domain] = &nz + zone = &nz + order = append(order, cand.Zone.Domain) + } + zone.Records = append(zone.Records, cand.Zone.Records...) + } + + var out []nmdata.CustomZone + for _, apex := range order { + zone := byApex[apex] + if len(zone.Records) == 0 { + continue + } + out = append(out, *zone) + } + return out +} + +func peerInDistributionGroups(peerGroups map[string]struct{}, groups []string) bool { + for _, g := range groups { + if _, ok := peerGroups[g]; ok { + return true + } + } + return false +} + +func filterGroupPeers(groups *map[string]*nmdata.Group, peers map[string]*nmdata.Peer) { + for groupID, groupInfo := range *groups { + filteredPeers := make([]string, 0, len(groupInfo.Peers)) + for _, pid := range groupInfo.Peers { + if _, exists := peers[pid]; exists { + filteredPeers = append(filteredPeers, pid) + } + } + + if len(filteredPeers) != len(groupInfo.Peers) { + ng := groupInfo.Copy() + ng.Peers = filteredPeers + (*groups)[groupID] = ng + } + } +} + +func filterPostureFailedPeers(postureFailedPeers *map[string]map[string]struct{}, policies []*nmdata.Policy, resourcePoliciesMap map[string][]*nmdata.Policy, peers map[string]*nmdata.Peer) { + if len(*postureFailedPeers) == 0 { + return + } + + referencedPostureChecks := make(map[string]struct{}) + for _, policy := range policies { + for _, checkID := range policy.SourcePostureChecks { + referencedPostureChecks[checkID] = struct{}{} + } + } + for _, resPolicies := range resourcePoliciesMap { + for _, policy := range resPolicies { + for _, checkID := range policy.SourcePostureChecks { + referencedPostureChecks[checkID] = struct{}{} + } + } + } + + for checkID, failedPeers := range *postureFailedPeers { + if _, referenced := referencedPostureChecks[checkID]; !referenced { + delete(*postureFailedPeers, checkID) + continue + } + for peerID := range failedPeers { + if _, exists := peers[peerID]; !exists { + delete(failedPeers, peerID) + } + } + if len(failedPeers) == 0 { + delete(*postureFailedPeers, checkID) + } + } +} + +func filterDNSRecordsByPeers(records []nmdata.SimpleRecord, peers map[string]*nmdata.Peer, includeIPv6 bool) []nmdata.SimpleRecord { + if len(records) == 0 || len(peers) == 0 { + return nil + } + + peerIPs := make(map[string]struct{}, len(peers)*2) + for _, peer := range peers { + if peer == nil { + continue + } + peerIPs[peer.IP.String()] = struct{}{} + if includeIPv6 && peer.IPv6.IsValid() { + peerIPs[peer.IPv6.String()] = struct{}{} + } + } + + filteredRecords := make([]nmdata.SimpleRecord, 0, len(records)) + for _, record := range records { + if _, exists := peerIPs[record.RData]; exists { + filteredRecords = append(filteredRecords, record) + } + } + + return filteredRecords +} + +func filterGroupIDToUserIDs(fullMap map[string][]string, neededGroupIDs map[string]struct{}) map[string][]string { + if len(neededGroupIDs) == 0 { + return nil + } + + filtered := make(map[string][]string, len(neededGroupIDs)) + for groupID := range neededGroupIDs { + if users, ok := fullMap[groupID]; ok { + filtered[groupID] = users + } + } + return filtered +} diff --git a/shared/management/networkmap/networkmapdata.go b/shared/management/networkmap/networkmapdata.go new file mode 100644 index 000000000..9193daa41 --- /dev/null +++ b/shared/management/networkmap/networkmapdata.go @@ -0,0 +1,55 @@ +package networkmap + +import ( + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" +) + +// NetworkMapData is a dependency-light, slim twin of the server Account. It +// carries only the state GetPeerNetworkMapComponents needs, expressed in the +// fresh nmdata twin types. A builder converts an Account into a NetworkMapData +// once per account; the per-peer components calculation then runs on this twin +// with no reference back to the Account. +type NetworkMapData struct { + Peers map[string]*nmdata.Peer + Groups map[string]*nmdata.Group + Policies []*nmdata.Policy + Routes []*nmdata.Route + NameServerGroups []*nmdata.NameServerGroup + NetworkResources []*nmdata.NetworkResource + + Network *nmdata.Network + DNSSettings *nmdata.DNSSettings + AccountSettings *nmdata.AccountSettingsInfo + + PostureChecks map[string]*nmdata.PostureChecks + + AllowedUserIDs map[string]struct{} + NetworkXIDToPublicID map[string]string + PostureCheckXIDToPublicID map[string]string + ValidatedPeers map[string]struct{} + ResourcePolicies map[string][]*nmdata.Policy + Routers map[string]map[string]*nmdata.NetworkRouter + GroupIDToUserIDs map[string][]string + DNSDomain string + + AppliedZoneCandidates []AppliedZoneCandidate + PrivateServiceCandidates []PrivateServiceCandidate +} + +// AppliedZoneCandidate is an account-level custom DNS zone reduced to the +// per-peer decision the components calc still makes: include the zone only when +// the peer belongs to one of its distribution groups. Record conversion is done +// once at build time. +type AppliedZoneCandidate struct { + DistributionGroups []string + Zone nmdata.CustomZone +} + +// PrivateServiceCandidate is a single private service's synthesized records, +// carried per apex zone. The builder resolves proxy-cluster connectivity and +// domain-suffix matching once; the calc merges the candidates whose AccessGroups +// the peer belongs to, grouped by Zone.Domain. +type PrivateServiceCandidate struct { + AccessGroups []string + Zone nmdata.CustomZone +} diff --git a/shared/management/networkmap/nmdata/account_settings.go b/shared/management/networkmap/nmdata/account_settings.go new file mode 100644 index 000000000..fefc3d81c --- /dev/null +++ b/shared/management/networkmap/nmdata/account_settings.go @@ -0,0 +1,11 @@ +package nmdata + +import "time" + +// AccountSettingsInfo is the slim twin of types.AccountSettingsInfo. +type AccountSettingsInfo struct { + PeerLoginExpirationEnabled bool + PeerLoginExpiration time.Duration + PeerInactivityExpirationEnabled bool + PeerInactivityExpiration time.Duration +} diff --git a/shared/management/networkmap/nmdata/dns.go b/shared/management/networkmap/nmdata/dns.go new file mode 100644 index 000000000..fe681af1a --- /dev/null +++ b/shared/management/networkmap/nmdata/dns.go @@ -0,0 +1,18 @@ +package nmdata + +// SimpleRecord is the slim twin of dns.SimpleRecord. +type SimpleRecord struct { + Name string + Type int + Class string + TTL int + RData string +} + +// CustomZone is the slim twin of dns.CustomZone. +type CustomZone struct { + Domain string + Records []SimpleRecord + SearchDomainDisabled bool + NonAuthoritative bool +} diff --git a/shared/management/networkmap/nmdata/dns_settings.go b/shared/management/networkmap/nmdata/dns_settings.go new file mode 100644 index 000000000..69fd5f517 --- /dev/null +++ b/shared/management/networkmap/nmdata/dns_settings.go @@ -0,0 +1,6 @@ +package nmdata + +// DNSSettings is the slim twin of types.DNSSettings. +type DNSSettings struct { + DisabledManagementGroups []string +} diff --git a/shared/management/networkmap/nmdata/group.go b/shared/management/networkmap/nmdata/group.go new file mode 100644 index 000000000..a2099e621 --- /dev/null +++ b/shared/management/networkmap/nmdata/group.go @@ -0,0 +1,24 @@ +package nmdata + +import "slices" + +const groupAllName = "All" + +// Group is the slim twin of types.Group. +type Group struct { + Name string + PublicID string + Peers []string +} + +func (g *Group) IsGroupAll() bool { + return g.Name == groupAllName +} + +func (g *Group) Copy() *Group { + return &Group{ + Name: g.Name, + PublicID: g.PublicID, + Peers: slices.Clone(g.Peers), + } +} diff --git a/shared/management/networkmap/nmdata/nameserver.go b/shared/management/networkmap/nmdata/nameserver.go new file mode 100644 index 000000000..2698dd8d0 --- /dev/null +++ b/shared/management/networkmap/nmdata/nameserver.go @@ -0,0 +1,24 @@ +package nmdata + +import "net/netip" + +// NameServerGroup is the slim twin of dns.NameServerGroup. +type NameServerGroup struct { + ID string + PublicID string + Name string + Description string + NameServers []NameServer + Groups []string + Primary bool + Domains []string + Enabled bool + SearchDomainsEnabled bool +} + +// NameServer is the slim twin of dns.NameServer. +type NameServer struct { + IP netip.Addr + NSType int + Port int +} diff --git a/shared/management/networkmap/nmdata/network.go b/shared/management/networkmap/nmdata/network.go new file mode 100644 index 000000000..4db8b1c45 --- /dev/null +++ b/shared/management/networkmap/nmdata/network.go @@ -0,0 +1,16 @@ +package nmdata + +import "net" + +// Network is the slim twin of types.Network. +type Network struct { + Identifier string + Net net.IPNet + NetV6 net.IPNet + Dns string + Serial uint64 +} + +func (n *Network) CurrentSerial() uint64 { + return n.Serial +} diff --git a/shared/management/networkmap/nmdata/network_resource.go b/shared/management/networkmap/nmdata/network_resource.go new file mode 100644 index 000000000..ba3358b18 --- /dev/null +++ b/shared/management/networkmap/nmdata/network_resource.go @@ -0,0 +1,18 @@ +package nmdata + +import "net/netip" + +// NetworkResource is the slim twin of resources/types.NetworkResource. +type NetworkResource struct { + ID string + NetworkID string + AccountID string + PublicID string + Name string + Description string + Type string + Address string + Domain string + Prefix netip.Prefix + Enabled bool +} diff --git a/shared/management/networkmap/nmdata/network_router.go b/shared/management/networkmap/nmdata/network_router.go new file mode 100644 index 000000000..fd5df37c4 --- /dev/null +++ b/shared/management/networkmap/nmdata/network_router.go @@ -0,0 +1,10 @@ +package nmdata + +// NetworkRouter is the slim twin of routers/types.NetworkRouter. +type NetworkRouter struct { + PublicID string + PeerGroups []string + Masquerade bool + Metric int + Enabled bool +} diff --git a/shared/management/networkmap/nmdata/peer.go b/shared/management/networkmap/nmdata/peer.go new file mode 100644 index 000000000..a0ee43fc1 --- /dev/null +++ b/shared/management/networkmap/nmdata/peer.go @@ -0,0 +1,105 @@ +package nmdata + +import ( + "net" + "net/netip" + "slices" + "time" +) + +const ( + peerCapabilitySourcePrefixes int32 = 1 + peerCapabilityIPv6Overlay int32 = 2 +) + +// Peer is the slim twin of peer.Peer. +type Peer struct { + ID string + Key string + SSHKey string + DNSLabel string + UserID string + SSHEnabled bool + LoginExpirationEnabled bool + LastLogin *time.Time + IP netip.Addr + IPv6 netip.Addr + Meta PeerSystemMeta + Location PeerLocation +} + +// PeerSystemMeta is the slim twin of peer.PeerSystemMeta. +type PeerSystemMeta struct { + WtVersion string + GoOS string + OSVersion string + KernelVersion string + NetworkAddresses []NetworkAddress + Files []File + Capabilities []int32 + Flags Flags +} + +// Flags is the slim twin of peer.Flags. +type Flags struct { + ServerSSHAllowed bool + DisableIPv6 bool +} + +// NetworkAddress is the slim twin of peer.NetworkAddress. +type NetworkAddress struct { + NetIP netip.Prefix +} + +// File is the slim twin of peer.File. +type File struct { + Path string + ProcessIsRunning bool +} + +// PeerLocation is the slim twin of peer.Location. +type PeerLocation struct { + CountryCode string + CityName string + ConnectionIP net.IP +} + +func (p *Peer) HasCapability(capability int32) bool { + return slices.Contains(p.Meta.Capabilities, capability) +} + +func (p *Peer) SupportsIPv6() bool { + return !p.Meta.Flags.DisableIPv6 && p.HasCapability(peerCapabilityIPv6Overlay) +} + +func (p *Peer) SupportsSourcePrefixes() bool { + return p.HasCapability(peerCapabilitySourcePrefixes) +} + +func (p *Peer) AddedWithSSOLogin() bool { + return p.UserID != "" +} + +func (p *Peer) FQDN(dnsDomain string) string { + if dnsDomain == "" { + return "" + } + return p.DNSLabel + "." + dnsDomain +} + +func (p *Peer) GetLastLogin() time.Time { + if p.LastLogin != nil { + return *p.LastLogin + } + return time.Time{} +} + +func (p *Peer) LoginExpired(expiresIn time.Duration) (bool, time.Duration) { + if !p.AddedWithSSOLogin() || !p.LoginExpirationEnabled { + return false, 0 + } + expiresAt := p.GetLastLogin().Add(expiresIn) + now := time.Now() + timeLeft := expiresAt.Sub(now) + return timeLeft <= 0, timeLeft +} diff --git a/shared/management/networkmap/nmdata/policy.go b/shared/management/networkmap/nmdata/policy.go new file mode 100644 index 000000000..f2f5af408 --- /dev/null +++ b/shared/management/networkmap/nmdata/policy.go @@ -0,0 +1,93 @@ +package nmdata + +const ( + policyRuleProtocolALL = "all" + policyRuleProtocolTCP = "tcp" + + defaultSSHPortString = "22" + nativeSSHPortString = "22022" + defaultSSHPortNumber uint16 = 22 + nativeSSHPortNumber uint16 = 22022 +) + +// Policy is the slim twin of types.Policy. +type Policy struct { + ID string + PublicID string + Enabled bool + SourcePostureChecks []string + Rules []*PolicyRule +} + +// PolicyRule is the slim twin of types.PolicyRule. +type PolicyRule struct { + ID string + PolicyID string + Enabled bool + Action string + Protocol string + Bidirectional bool + Sources []string + Destinations []string + SourceResource Resource + DestinationResource Resource + Ports []string + PortRanges []RulePortRange + AuthorizedGroups map[string][]string + AuthorizedUser string +} + +// RulePortRange is the slim twin of types.RulePortRange. +type RulePortRange struct { + Start uint16 + End uint16 +} + +// Resource is the slim twin of types.Resource. +type Resource struct { + ID string + Type string +} + +func (p *Policy) SourceGroups() []string { + if len(p.Rules) == 1 { + return p.Rules[0].Sources + } + groups := make(map[string]struct{}, len(p.Rules)) + for _, rule := range p.Rules { + for _, source := range rule.Sources { + groups[source] = struct{}{} + } + } + + groupIDs := make([]string, 0, len(groups)) + for groupID := range groups { + groupIDs = append(groupIDs, groupID) + } + + return groupIDs +} + +// PolicyRuleImpliesLegacySSH is the twin-typed sibling of types.PolicyRuleImpliesLegacySSH. +func PolicyRuleImpliesLegacySSH(rule *PolicyRule) bool { + return rule.Protocol == policyRuleProtocolALL || + (rule.Protocol == policyRuleProtocolTCP && (portsIncludesSSH(rule.Ports) || portRangeIncludesSSH(rule.PortRanges))) +} + +func portRangeIncludesSSH(portRanges []RulePortRange) bool { + for _, pr := range portRanges { + if (pr.Start <= defaultSSHPortNumber && pr.End >= defaultSSHPortNumber) || (pr.Start <= nativeSSHPortNumber && pr.End >= nativeSSHPortNumber) { + return true + } + } + return false +} + +func portsIncludesSSH(ports []string) bool { + for _, port := range ports { + if port == defaultSSHPortString || port == nativeSSHPortString { + return true + } + } + return false +} diff --git a/shared/management/networkmap/nmdata/posture.go b/shared/management/networkmap/nmdata/posture.go new file mode 100644 index 000000000..c8fb08fd9 --- /dev/null +++ b/shared/management/networkmap/nmdata/posture.go @@ -0,0 +1,59 @@ +package nmdata + +const ( + checkActionAllow = "allow" + checkActionDeny = "deny" +) + +// PostureChecks is the slim twin of posture.Checks. +type PostureChecks struct { + ID string + Checks ChecksDefinition +} + +// ChecksDefinition is the slim twin of posture.ChecksDefinition. +type ChecksDefinition struct { + NBVersionCheck *NBVersionCheck + OSVersionCheck *OSVersionCheck + GeoLocationCheck *GeoLocationCheck + PeerNetworkRangeCheck *PeerNetworkRangeCheck + ProcessCheck *ProcessCheck +} + +type postureCheck interface { + check(peer *Peer) (bool, error) +} + +// Passes reports whether the peer satisfies every check in this bundle. It +// mirrors the server posture path: a check returning (false, _) — including on +// an evaluation error — fails the bundle. +func (pc *PostureChecks) Passes(peer *Peer) bool { + for _, c := range pc.GetChecks() { + valid, _ := c.check(peer) + if !valid { + return false + } + } + return true +} + +// GetChecks returns the initialized checks in the same order as posture.Checks.GetChecks. +func (pc *PostureChecks) GetChecks() []postureCheck { + var checks []postureCheck + if pc.Checks.NBVersionCheck != nil { + checks = append(checks, pc.Checks.NBVersionCheck) + } + if pc.Checks.OSVersionCheck != nil { + checks = append(checks, pc.Checks.OSVersionCheck) + } + if pc.Checks.GeoLocationCheck != nil { + checks = append(checks, pc.Checks.GeoLocationCheck) + } + if pc.Checks.PeerNetworkRangeCheck != nil { + checks = append(checks, pc.Checks.PeerNetworkRangeCheck) + } + if pc.Checks.ProcessCheck != nil { + checks = append(checks, pc.Checks.ProcessCheck) + } + return checks +} diff --git a/shared/management/networkmap/nmdata/posture_geo_location.go b/shared/management/networkmap/nmdata/posture_geo_location.go new file mode 100644 index 000000000..18b0919b2 --- /dev/null +++ b/shared/management/networkmap/nmdata/posture_geo_location.go @@ -0,0 +1,45 @@ +package nmdata + +import "fmt" + +// GeoLocation is the slim twin of posture.Location. +type GeoLocation struct { + CountryCode string + CityName string +} + +// GeoLocationCheck is the slim twin of posture.GeoLocationCheck. +type GeoLocationCheck struct { + Locations []GeoLocation + Action string +} + +func (g *GeoLocationCheck) check(peer *Peer) (bool, error) { + if peer.Location.CountryCode == "" && peer.Location.CityName == "" { + return false, fmt.Errorf("peer's location is not set") + } + + for _, loc := range g.Locations { + if loc.CountryCode == peer.Location.CountryCode { + if loc.CityName == "" || loc.CityName == peer.Location.CityName { + switch g.Action { + case checkActionDeny: + return false, nil + case checkActionAllow: + return true, nil + default: + return false, fmt.Errorf("invalid geo location action: %s", g.Action) + } + } + } + } + + if g.Action == checkActionDeny { + return true, nil + } + if g.Action == checkActionAllow { + return false, nil + } + + return false, fmt.Errorf("invalid geo location action: %s", g.Action) +} diff --git a/shared/management/networkmap/nmdata/posture_nb_version.go b/shared/management/networkmap/nmdata/posture_nb_version.go new file mode 100644 index 000000000..3d82a4c80 --- /dev/null +++ b/shared/management/networkmap/nmdata/posture_nb_version.go @@ -0,0 +1,38 @@ +package nmdata + +import ( + "strings" + + "github.com/hashicorp/go-version" +) + +// NBVersionCheck is the slim twin of posture.NBVersionCheck. +type NBVersionCheck struct { + MinVersion string +} + +func (n *NBVersionCheck) check(peer *Peer) (bool, error) { + return meetsMinVersion(n.MinVersion, peer.Meta.WtVersion) +} + +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 +} + +func sanitizeVersion(v string) string { + parts := strings.Split(v, "-") + return parts[0] +} diff --git a/shared/management/networkmap/nmdata/posture_network.go b/shared/management/networkmap/nmdata/posture_network.go new file mode 100644 index 000000000..d8dd2cf00 --- /dev/null +++ b/shared/management/networkmap/nmdata/posture_network.go @@ -0,0 +1,62 @@ +package nmdata + +import ( + "fmt" + "net/netip" +) + +// PeerNetworkRangeCheck is the slim twin of posture.PeerNetworkRangeCheck. +type PeerNetworkRangeCheck struct { + Action string + Ranges []netip.Prefix +} + +func (p *PeerNetworkRangeCheck) check(peer *Peer) (bool, error) { + peerPrefixes := make([]netip.Prefix, 0, len(peer.Meta.NetworkAddresses)+1) + for _, peerNetAddr := range peer.Meta.NetworkAddresses { + peerPrefixes = append(peerPrefixes, peerNetAddr.NetIP) + } + if connIP := peer.Location.ConnectionIP; len(connIP) > 0 { + if addr, ok := netip.AddrFromSlice(connIP); ok { + addr = addr.Unmap() + peerPrefixes = append(peerPrefixes, netip.PrefixFrom(addr, addr.BitLen())) + } + } + + if len(peerPrefixes) == 0 { + return false, fmt.Errorf("peer's does not contain peer network range addresses") + } + + for _, peerPrefix := range peerPrefixes { + for _, rangePrefix := range p.Ranges { + if !prefixContains(rangePrefix, peerPrefix) { + continue + } + switch p.Action { + case checkActionDeny: + return false, nil + case checkActionAllow: + return true, nil + default: + return false, fmt.Errorf("invalid peer network range check action: %s", p.Action) + } + } + } + + if p.Action == checkActionDeny { + return true, nil + } + if p.Action == checkActionAllow { + return false, nil + } + + return false, fmt.Errorf("invalid peer network range check action: %s", p.Action) +} + +func prefixContains(outer, inner netip.Prefix) bool { + outer = outer.Masked() + inner = inner.Masked() + return outer.Bits() <= inner.Bits() && + outer.Addr().BitLen() == inner.Addr().BitLen() && + outer.Contains(inner.Addr()) +} diff --git a/shared/management/networkmap/nmdata/posture_os_version.go b/shared/management/networkmap/nmdata/posture_os_version.go new file mode 100644 index 000000000..779bd2ac3 --- /dev/null +++ b/shared/management/networkmap/nmdata/posture_os_version.go @@ -0,0 +1,79 @@ +package nmdata + +import ( + "strings" + + "github.com/hashicorp/go-version" +) + +// MinVersionCheck is the slim twin of posture.MinVersionCheck. +type MinVersionCheck struct { + MinVersion string +} + +// MinKernelVersionCheck is the slim twin of posture.MinKernelVersionCheck. +type MinKernelVersionCheck struct { + MinKernelVersion string +} + +// OSVersionCheck is the slim twin of posture.OSVersionCheck. +type OSVersionCheck struct { + Android *MinVersionCheck + Darwin *MinVersionCheck + Ios *MinVersionCheck + Linux *MinKernelVersionCheck + Windows *MinKernelVersionCheck +} + +func (c *OSVersionCheck) check(peer *Peer) (bool, error) { + switch peer.Meta.GoOS { + case "android": + return checkMinVersion(peer.Meta.OSVersion, c.Android) + case "darwin": + return checkMinVersion(peer.Meta.OSVersion, c.Darwin) + case "ios": + return checkMinVersion(peer.Meta.OSVersion, c.Ios) + case "linux": + kernelVersion := strings.Split(peer.Meta.KernelVersion, "-")[0] + return checkMinKernelVersion(kernelVersion, c.Linux) + case "windows": + return checkMinKernelVersion(peer.Meta.KernelVersion, c.Windows) + } + return true, nil +} + +func checkMinVersion(peerVersion string, check *MinVersionCheck) (bool, error) { + if check == nil { + return false, nil + } + + peerNBVersion, err := version.NewVersion(peerVersion) + if err != nil { + return false, err + } + + constraints, err := version.NewConstraint(">= " + check.MinVersion) + if err != nil { + return false, err + } + + return constraints.Check(peerNBVersion), nil +} + +func checkMinKernelVersion(peerVersion string, check *MinKernelVersionCheck) (bool, error) { + if check == nil { + return false, nil + } + + peerNBVersion, err := version.NewVersion(peerVersion) + if err != nil { + return false, err + } + + constraints, err := version.NewConstraint(">= " + check.MinKernelVersion) + if err != nil { + return false, err + } + + return constraints.Check(peerNBVersion), nil +} diff --git a/shared/management/networkmap/nmdata/posture_process.go b/shared/management/networkmap/nmdata/posture_process.go new file mode 100644 index 000000000..3d35613b5 --- /dev/null +++ b/shared/management/networkmap/nmdata/posture_process.go @@ -0,0 +1,56 @@ +package nmdata + +import ( + "fmt" + "slices" +) + +// Process is the slim twin of posture.Process. +type Process struct { + LinuxPath string + MacPath string + WindowsPath string +} + +// ProcessCheck is the slim twin of posture.ProcessCheck. +type ProcessCheck struct { + Processes []Process +} + +func (p *ProcessCheck) check(peer *Peer) (bool, error) { + peerActiveProcesses := extractPeerActiveProcesses(peer.Meta.Files) + + var pathSelector func(Process) string + switch peer.Meta.GoOS { + case "linux": + pathSelector = func(process Process) string { return process.LinuxPath } + case "darwin": + pathSelector = func(process Process) string { return process.MacPath } + case "windows": + pathSelector = func(process Process) string { return process.WindowsPath } + default: + return false, fmt.Errorf("unsupported peer's operating system: %s", peer.Meta.GoOS) + } + + return p.areAllProcessesRunning(peerActiveProcesses, pathSelector), nil +} + +func (p *ProcessCheck) areAllProcessesRunning(activeProcesses []string, pathSelector func(Process) string) bool { + for _, process := range p.Processes { + path := pathSelector(process) + if path == "" || !slices.Contains(activeProcesses, path) { + return false + } + } + return true +} + +func extractPeerActiveProcesses(files []File) []string { + activeProcesses := make([]string, 0, len(files)) + for _, file := range files { + if file.ProcessIsRunning { + activeProcesses = append(activeProcesses, file.Path) + } + } + return activeProcesses +} diff --git a/shared/management/networkmap/nmdata/route.go b/shared/management/networkmap/nmdata/route.go new file mode 100644 index 000000000..e2301f094 --- /dev/null +++ b/shared/management/networkmap/nmdata/route.go @@ -0,0 +1,108 @@ +package nmdata + +import ( + "net/netip" + "slices" + "strings" + + "github.com/netbirdio/netbird/shared/management/domain" +) + +// NetworkType mirrors route.NetworkType iota values. +const ( + NetworkTypeInvalid = 0 + NetworkTypeIPv4 = 1 + NetworkTypeIPv6 = 2 + NetworkTypeDomain = 3 + + haSeparator = "|" +) + +// Route is the slim twin of route.Route. +type Route struct { + ID string + AccountID string + PublicID string + Network netip.Prefix + Domains domain.List + KeepRoute bool + NetID string + Description string + Peer string + PeerID string + PeerGroups []string + NetworkType int + Masquerade bool + Metric int + Enabled bool + Groups []string + AccessControlGroups []string + SkipAutoApply bool +} + +func (r *Route) Equal(other *Route) bool { + if r == nil && other == nil { + return true + } else if r == nil || other == nil { + return false + } + + return other.ID == r.ID && + other.Description == r.Description && + other.NetID == r.NetID && + other.Network == r.Network && + slices.Equal(r.Domains, other.Domains) && + other.KeepRoute == r.KeepRoute && + other.NetworkType == r.NetworkType && + other.Peer == r.Peer && + other.PeerID == r.PeerID && + other.Metric == r.Metric && + other.Masquerade == r.Masquerade && + other.Enabled == r.Enabled && + slices.Equal(r.Groups, other.Groups) && + slices.Equal(r.PeerGroups, other.PeerGroups) && + slices.Equal(r.AccessControlGroups, other.AccessControlGroups) && + other.SkipAutoApply == r.SkipAutoApply +} + +func (r *Route) IsDynamic() bool { + return r.NetworkType == NetworkTypeDomain +} + +func (r *Route) NetString() string { + if r.IsDynamic() && r.Domains != nil { + return r.Domains.SafeString() + } + return r.Network.String() +} + +func (r *Route) GetHAUniqueID() string { + return r.NetID + haSeparator + r.NetString() +} + +func (r *Route) GetResourceID() string { + return strings.Split(r.ID, ":")[0] +} + +func (r *Route) Copy() *Route { + return &Route{ + ID: r.ID, + AccountID: r.AccountID, + PublicID: r.PublicID, + Network: r.Network, + Domains: slices.Clone(r.Domains), + KeepRoute: r.KeepRoute, + NetID: r.NetID, + Description: r.Description, + Peer: r.Peer, + PeerID: r.PeerID, + PeerGroups: slices.Clone(r.PeerGroups), + NetworkType: r.NetworkType, + Masquerade: r.Masquerade, + Metric: r.Metric, + Enabled: r.Enabled, + Groups: slices.Clone(r.Groups), + AccessControlGroups: slices.Clone(r.AccessControlGroups), + SkipAutoApply: r.SkipAutoApply, + } +} diff --git a/shared/management/types/firewall_helpers.go b/shared/management/types/firewall_helpers.go index dd174abe4..6969f8147 100644 --- a/shared/management/types/firewall_helpers.go +++ b/shared/management/types/firewall_helpers.go @@ -3,8 +3,8 @@ package types import ( "strconv" - nbpeer "github.com/netbirdio/netbird/management/server/peer" "github.com/netbirdio/netbird/management/server/posture" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" "github.com/netbirdio/netbird/version" ) @@ -48,7 +48,7 @@ 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 { +func ExpandPortsAndRanges(base FirewallRule, rule *nmdata.PolicyRule, peer *nmdata.Peer) []*FirewallRule { features := peerSupportedFirewallFeatures(peer.Meta.WtVersion) var expanded []*FirewallRule @@ -66,7 +66,7 @@ func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer fr := base if features.portRanges { - fr.PortRange = portRange + fr.PortRange = RulePortRange{Start: portRange.Start, End: portRange.End} } else { if portRange.Start != portRange.End { continue @@ -76,7 +76,7 @@ func ExpandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer expanded = append(expanded, &fr) } - if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == PolicyRuleProtocolNetbirdSSH { + if shouldCheckRulesForNativeSSH(features.nativeSSH, rule, peer) || rule.Protocol == string(PolicyRuleProtocolNetbirdSSH) { expanded = addNativeSSHRule(base, expanded) } @@ -106,8 +106,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 *nmdata.PolicyRule, peer *nmdata.Peer) bool { + return supportsNative && peer.SSHEnabled && peer.Meta.Flags.ServerSSHAllowed && rule.Protocol == string(PolicyRuleProtocolTCP) } func peerSupportedFirewallFeatures(peerVer string) supportedFeatures { diff --git a/shared/management/types/firewall_rule.go b/shared/management/types/firewall_rule.go index 87dcfe307..2efedf625 100644 --- a/shared/management/types/firewall_rule.go +++ b/shared/management/types/firewall_rule.go @@ -9,8 +9,8 @@ import ( log "github.com/sirupsen/logrus" - nbpeer "github.com/netbirdio/netbird/management/server/peer" nbroute "github.com/netbirdio/netbird/route" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) const ( @@ -51,7 +51,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 *nmdata.Route, rule *nmdata.PolicyRule, groupPeers []*nmdata.Peer, direction int, includeIPv6 bool) []*RouteFirewallRule { rulesExists := make(map[string]struct{}) rules := make([]*RouteFirewallRule, 0) @@ -72,11 +72,11 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule baseRule := RouteFirewallRule{ PolicyID: rule.PolicyID, - RouteID: route.ID, + RouteID: nbroute.ID(route.ID), SourceRanges: sourceRanges, - Action: string(rule.Action), + Action: rule.Action, Destination: route.Network.String(), - Protocol: string(rule.Protocol), + Protocol: rule.Protocol, Domains: route.Domains, IsDynamic: route.IsDynamic(), } @@ -94,7 +94,7 @@ func GenerateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule v6Rule.SourceRanges = v6Sources if isDefaultV4 { v6Rule.Destination = "::/0" - v6Rule.RouteID = route.ID + "-v6-default" + v6Rule.RouteID = nbroute.ID(route.ID + "-v6-default") } if len(rule.Ports) == 0 { rules = append(rules, generateRulesWithPortRanges(v6Rule, rule, rulesExists)...) @@ -107,7 +107,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 []*nmdata.Peer) (v4, v6 []string) { v4 = make([]string, 0, len(groupPeers)) v6 = make([]string, 0, len(groupPeers)) for _, peer := range groupPeers { @@ -123,7 +123,7 @@ func splitPeerSourcesByFamily(groupPeers []*nbpeer.Peer) (v4, v6 []string) { } // generateRulesForPeer generates rules for a given peer based on ports and port ranges. -func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule { +func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *nmdata.PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule { rules := make([]*RouteFirewallRule, 0) ruleIDBase := generateRuleIDBase(rule, baseRule) @@ -139,7 +139,7 @@ func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, r if _, ok := rulesExists[ruleID]; !ok { rulesExists[ruleID] = struct{}{} pr := baseRule - pr.PortRange = portRange + pr.PortRange = RulePortRange{Start: portRange.Start, End: portRange.End} rules = append(rules, &pr) } } @@ -151,7 +151,7 @@ func generateRulesWithPortRanges(baseRule RouteFirewallRule, rule *PolicyRule, r } // generateRulesWithPorts generates rules when specific ports are provided. -func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rule *PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule { +func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rule *nmdata.PolicyRule, rulesExists map[string]struct{}) []*RouteFirewallRule { rules := make([]*RouteFirewallRule, 0) ruleIDBase := generateRuleIDBase(rule, baseRule) @@ -177,6 +177,6 @@ func generateRulesWithPorts(ctx context.Context, baseRule RouteFirewallRule, rul } // generateRuleIDBase generates the base rule ID for checking duplicates. -func generateRuleIDBase(rule *PolicyRule, baseRule RouteFirewallRule) string { +func generateRuleIDBase(rule *nmdata.PolicyRule, baseRule RouteFirewallRule) string { return rule.ID + strings.Join(baseRule.SourceRanges, ",") + strconv.Itoa(FirewallRuleDirectionIN) + baseRule.Protocol + baseRule.Action } diff --git a/shared/management/types/firewall_rule_test.go b/shared/management/types/firewall_rule_test.go index 9de4ca04a..96fef3bd9 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" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) func TestSplitPeerSourcesByFamily(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*nmdata.Peer{ { 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 := []*nmdata.Peer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), @@ -46,15 +45,15 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) { }, } - r := &route.Route{ + r := &nmdata.Route{ ID: "route1", Network: netip.MustParsePrefix("10.0.0.0/24"), } - rule := &PolicyRule{ + rule := &nmdata.PolicyRule{ PolicyID: "policy1", ID: "rule1", - Action: PolicyTrafficActionAccept, - Protocol: PolicyRuleProtocolALL, + Action: string(PolicyTrafficActionAccept), + Protocol: string(PolicyRuleProtocolALL), } rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) @@ -65,7 +64,7 @@ func TestGenerateRouteFirewallRules_V4Route(t *testing.T) { } func TestGenerateRouteFirewallRules_V6Route(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*nmdata.Peer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), @@ -75,15 +74,15 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) { }, } - r := &route.Route{ + r := &nmdata.Route{ ID: "route1", Network: netip.MustParsePrefix("2001:db8::/32"), } - rule := &PolicyRule{ + rule := &nmdata.PolicyRule{ PolicyID: "policy1", ID: "rule1", - Action: PolicyTrafficActionAccept, - Protocol: PolicyRuleProtocolALL, + Action: string(PolicyTrafficActionAccept), + Protocol: string(PolicyRuleProtocolALL), } rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) @@ -93,7 +92,7 @@ func TestGenerateRouteFirewallRules_V6Route(t *testing.T) { } func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*nmdata.Peer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), @@ -103,16 +102,16 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) { }, } - r := &route.Route{ + r := &nmdata.Route{ ID: "route1", - NetworkType: route.DomainNetwork, + NetworkType: nmdata.NetworkTypeDomain, Domains: domain.List{"example.com"}, } - rule := &PolicyRule{ + rule := &nmdata.PolicyRule{ PolicyID: "policy1", ID: "rule1", - Action: PolicyTrafficActionAccept, - Protocol: PolicyRuleProtocolALL, + Action: string(PolicyTrafficActionAccept), + Protocol: string(PolicyRuleProtocolALL), } rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) @@ -126,21 +125,21 @@ func TestGenerateRouteFirewallRules_DynamicRoute_DualStack(t *testing.T) { } func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*nmdata.Peer{ {IP: netip.MustParseAddr("100.64.0.1")}, {IP: netip.MustParseAddr("100.64.0.2")}, } - r := &route.Route{ + r := &nmdata.Route{ ID: "route1", - NetworkType: route.DomainNetwork, + NetworkType: nmdata.NetworkTypeDomain, Domains: domain.List{"example.com"}, } - rule := &PolicyRule{ + rule := &nmdata.PolicyRule{ PolicyID: "policy1", ID: "rule1", - Action: PolicyTrafficActionAccept, - Protocol: PolicyRuleProtocolALL, + Action: string(PolicyTrafficActionAccept), + Protocol: string(PolicyRuleProtocolALL), } rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, true) @@ -150,7 +149,7 @@ func TestGenerateRouteFirewallRules_DynamicRoute_NoV6Peers(t *testing.T) { } func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) { - peers := []*nbpeer.Peer{ + peers := []*nmdata.Peer{ { IP: netip.MustParseAddr("100.64.0.1"), IPv6: netip.MustParseAddr("fd00::1"), @@ -162,15 +161,15 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) { } t.Run("v6 route excluded", func(t *testing.T) { - r := &route.Route{ + r := &nmdata.Route{ ID: "route1", Network: netip.MustParsePrefix("2001:db8::/32"), } - rule := &PolicyRule{ + rule := &nmdata.PolicyRule{ PolicyID: "policy1", ID: "rule1", - Action: PolicyTrafficActionAccept, - Protocol: PolicyRuleProtocolALL, + Action: string(PolicyTrafficActionAccept), + Protocol: string(PolicyRuleProtocolALL), } rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false) @@ -178,16 +177,16 @@ func TestGenerateRouteFirewallRules_IncludeIPv6False(t *testing.T) { }) t.Run("dynamic route only v4", func(t *testing.T) { - r := &route.Route{ + r := &nmdata.Route{ ID: "route1", - NetworkType: route.DomainNetwork, + NetworkType: nmdata.NetworkTypeDomain, Domains: domain.List{"example.com"}, } - rule := &PolicyRule{ + rule := &nmdata.PolicyRule{ PolicyID: "policy1", ID: "rule1", - Action: PolicyTrafficActionAccept, - Protocol: PolicyRuleProtocolALL, + Action: string(PolicyTrafficActionAccept), + Protocol: string(PolicyRuleProtocolALL), } rules := GenerateRouteFirewallRules(context.Background(), r, rule, peers, FirewallRuleDirectionIN, false) diff --git a/shared/management/types/network.go b/shared/management/types/network.go index fe67bfd97..1f662ffd9 100644 --- a/shared/management/types/network.go +++ b/shared/management/types/network.go @@ -15,9 +15,8 @@ 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/networkmap/nmdata" "github.com/netbirdio/netbird/shared/management/proto" "github.com/netbirdio/netbird/shared/management/status" ) @@ -39,11 +38,11 @@ const ( ) type NetworkMap struct { - Peers []*nbpeer.Peer - Network *Network - Routes []*route.Route + Peers []*nmdata.Peer + Network *nmdata.Network + Routes []*nmdata.Route DNSConfig nbdns.Config - OfflinePeers []*nbpeer.Peer + OfflinePeers []*nmdata.Peer FirewallRules []*FirewallRule RoutesFirewallRules []*RouteFirewallRule ForwardingRules []*ForwardingRule @@ -60,8 +59,8 @@ func (nm *NetworkMap) Merge(other *NetworkMap) { nm.ForwardingRules = util.MergeUnique(nm.ForwardingRules, other.ForwardingRules) } -func mergeUniquePeersByID(peers1, peers2 []*nbpeer.Peer) []*nbpeer.Peer { - result := make(map[string]*nbpeer.Peer) +func mergeUniquePeersByID(peers1, peers2 []*nmdata.Peer) []*nmdata.Peer { + result := make(map[string]*nmdata.Peer) for _, peer := range peers1 { result[peer.ID] = peer } diff --git a/shared/management/types/networkmap_components.go b/shared/management/types/networkmap_components.go index fdb70f2f7..e28dbd095 100644 --- a/shared/management/types/networkmap_components.go +++ b/shared/management/types/networkmap_components.go @@ -13,36 +13,35 @@ 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" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) type NetworkMapComponents struct { PeerID string - Network *Network - AccountSettings *AccountSettingsInfo - DNSSettings *DNSSettings + Network *nmdata.Network + AccountSettings *nmdata.AccountSettingsInfo + DNSSettings *nmdata.DNSSettings CustomZoneDomain string - Peers map[string]*nbpeer.Peer - Groups map[string]*Group - 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 + Peers map[string]*nmdata.Peer + Groups map[string]*nmdata.Group + Policies []*nmdata.Policy + Routes []*nmdata.Route + NameServerGroups []*nmdata.NameServerGroup + AllDNSRecords []nmdata.SimpleRecord + AccountZones []nmdata.CustomZone + ResourcePoliciesMap map[string][]*nmdata.Policy + RoutersMap map[string]map[string]*nmdata.NetworkRouter + NetworkResources []*nmdata.NetworkResource GroupIDToUserIDs map[string][]string AllowedUserIDs map[string]struct{} PostureFailedPeers map[string]map[string]struct{} - RouterPeers map[string]*nbpeer.Peer + RouterPeers map[string]*nmdata.Peer // NetworkXIDToPublicID maps Network.ID (xid) → PublicID. // Consumed by the envelope encoder to @@ -62,7 +61,7 @@ type NetworkMapComponents struct { } type routeIndexEntry struct { - route *route.Route + route *nmdata.Route viaGroup bool } @@ -78,15 +77,15 @@ func EmptyNetworkMapComponents(nm *NetworkMapComponents) *NetworkMapComponents { return nm } -func (c *NetworkMapComponents) GetPeerInfo(peerID string) *nbpeer.Peer { +func (c *NetworkMapComponents) GetPeerInfo(peerID string) *nmdata.Peer { return c.Peers[peerID] } -func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *nbpeer.Peer { +func (c *NetworkMapComponents) GetRouterPeerInfo(peerID string) *nmdata.Peer { return c.RouterPeers[peerID] } -func (c *NetworkMapComponents) GetGroupInfo(groupID string) *Group { +func (c *NetworkMapComponents) GetGroupInfo(groupID string) *nmdata.Group { return c.Groups[groupID] } @@ -141,7 +140,7 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap { peersToConnect, expiredPeers := c.filterPeersByLoginExpiration(aclPeers) includeIPv6 := false - if p := c.Peers[targetPeerID]; p != nil { + if p := c.GetPeerInfo(targetPeerID); p != nil { includeIPv6 = p.SupportsIPv6() && p.IPv6.IsValid() } routesUpdate := filterAndExpandRoutes(c.getRoutesToSync(targetPeerID, peersToConnect, peerGroups), includeIPv6) @@ -173,11 +172,11 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap { if c.CustomZoneDomain != "" && len(c.AllDNSRecords) > 0 { customZones = append(customZones, nbdns.CustomZone{ Domain: c.CustomZoneDomain, - Records: c.AllDNSRecords, + Records: toRealRecords(c.AllDNSRecords), }) } - customZones = append(customZones, c.AccountZones...) + customZones = append(customZones, toRealZones(c.AccountZones)...) dnsUpdate.CustomZones = customZones dnsUpdate.NameServerGroups = c.getPeerNSGroupsFromGroups(targetPeerID, peerGroups) @@ -185,7 +184,7 @@ func (c *NetworkMapComponents) Calculate(ctx context.Context) *NetworkMap { return &NetworkMap{ Peers: peersToConnectIncludingRouters, - Network: c.Network.Copy(), + Network: c.Network, Routes: append(filterAndExpandRoutes(networkResourcesRoutes, includeIPv6), routesUpdate...), DNSConfig: dnsUpdate, OfflinePeers: expiredPeers, @@ -200,7 +199,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) ([]*nmdata.Peer, []*FirewallRule, map[string]map[string]struct{}, bool) { targetPeer := c.GetPeerInfo(targetPeerID) if targetPeer == nil { return nil, nil, nil, false @@ -220,16 +219,16 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ( continue } - var sourcePeers, destinationPeers []*nbpeer.Peer + var sourcePeers, destinationPeers []*nmdata.Peer var peerInSources, peerInDestinations bool - if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" { + if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" { sourcePeers, peerInSources = c.getPeerFromResource(rule.SourceResource, targetPeerID) } else { sourcePeers, peerInSources = c.getAllPeersFromGroups(rule.Sources, targetPeerID, policy.SourcePostureChecks) } - if rule.DestinationResource.Type == ResourceTypePeer && rule.DestinationResource.ID != "" { + if rule.DestinationResource.Type == string(ResourceTypePeer) && rule.DestinationResource.ID != "" { destinationPeers, peerInDestinations = c.getPeerFromResource(rule.DestinationResource, targetPeerID) } else { destinationPeers, peerInDestinations = c.getAllPeersFromGroups(rule.Destinations, targetPeerID, nil) @@ -252,7 +251,7 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ( generateResources(rule, sourcePeers, FirewallRuleDirectionIN) } - if peerInDestinations && rule.Protocol == PolicyRuleProtocolNetbirdSSH { + if peerInDestinations && rule.Protocol == string(PolicyRuleProtocolNetbirdSSH) { sshEnabled = true switch { case len(rule.AuthorizedGroups) > 0: @@ -283,7 +282,7 @@ func (c *NetworkMapComponents) getPeerConnectionResources(targetPeerID string) ( default: authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs() } - } else if peerInDestinations && PolicyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled { + } else if peerInDestinations && nmdata.PolicyRuleImpliesLegacySSH(rule) && targetPeer.SSHEnabled { sshEnabled = true authorizedUsers[auth.Wildcard] = c.getAllowedUserIDs() } @@ -303,19 +302,19 @@ 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 *nmdata.Peer) (func(*nmdata.PolicyRule, []*nmdata.Peer, int), func() ([]*nmdata.Peer, []*FirewallRule)) { rulesExists := make(map[string]struct{}) peersExists := make(map[string]struct{}) rules := make([]*FirewallRule, 0) - peers := make([]*nbpeer.Peer, 0) + peers := make([]*nmdata.Peer, 0) - return func(rule *PolicyRule, groupPeers []*nbpeer.Peer, direction int) { + return func(rule *nmdata.PolicyRule, groupPeers []*nmdata.Peer, direction int) { protocol := rule.Protocol - if protocol == PolicyRuleProtocolNetbirdSSH { - protocol = PolicyRuleProtocolTCP + if protocol == string(PolicyRuleProtocolNetbirdSSH) { + protocol = string(PolicyRuleProtocolTCP) } - protocolStr := string(protocol) + protocolStr := protocol actionStr := string(rule.Action) dirStr := strconv.Itoa(direction) portsJoined := strings.Join(rule.Ports, ",") @@ -361,15 +360,15 @@ func (c *NetworkMapComponents) connResourcesGenerator(targetPeer *nbpeer.Peer) ( PortsJoined: portsJoined, }) } - }, func() ([]*nbpeer.Peer, []*FirewallRule) { + }, func() ([]*nmdata.Peer, []*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) ([]*nmdata.Peer, bool) { peerInGroups := false uniquePeerIDs := c.getUniquePeerIDsFromGroupsIDs(groups) - filteredPeers := make([]*nbpeer.Peer, 0, len(uniquePeerIDs)) + filteredPeers := make([]*nmdata.Peer, 0, len(uniquePeerIDs)) for _, p := range uniquePeerIDs { peerInfo := c.GetPeerInfo(p) @@ -421,22 +420,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 nmdata.Resource, peerID string) ([]*nmdata.Peer, bool) { if resource.ID == peerID { - return []*nbpeer.Peer{}, true + return []*nmdata.Peer{}, true } peerInfo := c.GetPeerInfo(resource.ID) if peerInfo == nil { - return []*nbpeer.Peer{}, false + return []*nmdata.Peer{}, false } - return []*nbpeer.Peer{peerInfo}, false + return []*nmdata.Peer{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 []*nmdata.Peer) ([]*nmdata.Peer, []*nmdata.Peer) { + peersToConnect := make([]*nmdata.Peer, 0, len(aclPeers)) + var expiredPeers []*nmdata.Peer for _, p := range aclPeers { expired, _ := p.LoginExpired(c.AccountSettings.PeerLoginExpiration) @@ -476,7 +475,7 @@ func (c *NetworkMapComponents) getPeerNSGroupsFromGroups(peerID string, groupLis for _, gID := range nsGroup.Groups { if _, found := groupList[gID]; found { if !c.peerIsNameserver(peerIPStr, nsGroup) { - peerNSGroups = append(peerNSGroups, nsGroup.Copy()) + peerNSGroups = append(peerNSGroups, toRealNSGroup(nsGroup)) } break } @@ -486,7 +485,7 @@ func (c *NetworkMapComponents) getPeerNSGroupsFromGroups(peerID string, groupLis return peerNSGroups } -func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nbdns.NameServerGroup) bool { +func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nmdata.NameServerGroup) bool { for _, ns := range nsGroup.NameServers { if peerIPStr == ns.IP.String() { return true @@ -498,8 +497,8 @@ func (c *NetworkMapComponents) peerIsNameserver(peerIPStr string, nsGroup *nbdns // filterAndExpandRoutes drops v6 routes for non-capable peers and duplicates // the default v4 route (0.0.0.0/0) as ::/0 for v6-capable peers. // TODO: the "-v6" suffix on IDs could collide with user-supplied route IDs. -func filterAndExpandRoutes(routes []*route.Route, includeIPv6 bool) []*route.Route { - filtered := make([]*route.Route, 0, len(routes)) +func filterAndExpandRoutes(routes []*nmdata.Route, includeIPv6 bool) []*nmdata.Route { + filtered := make([]*nmdata.Route, 0, len(routes)) for _, r := range routes { if !includeIPv6 && r.Network.Addr().Is6() { continue @@ -511,14 +510,14 @@ func filterAndExpandRoutes(routes []*route.Route, includeIPv6 bool) []*route.Rou v6.ID = r.ID + "-v6-default" v6.NetID = r.NetID + "-v6" v6.Network = netip.MustParsePrefix("::/0") - v6.NetworkType = route.IPv6Network + v6.NetworkType = nmdata.NetworkTypeIPv6 filtered = append(filtered, v6) } } return filtered } -func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*nbpeer.Peer, peerGroups LookupMap) []*route.Route { +func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*nmdata.Peer, peerGroups LookupMap) []*nmdata.Route { routes, peerDisabledRoutes := c.getRoutingPeerRoutes(peerID) peerRoutesMembership := make(LookupMap) for _, r := range append(routes, peerDisabledRoutes...) { @@ -535,7 +534,7 @@ func (c *NetworkMapComponents) getRoutesToSync(peerID string, aclPeers []*nbpeer return routes } -func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoutes []*route.Route, disabledRoutes []*route.Route) { +func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoutes []*nmdata.Route, disabledRoutes []*nmdata.Route) { peerInfo := c.GetPeerInfo(peerID) if peerInfo == nil { peerInfo = c.GetRouterPeerInfo(peerID) @@ -544,9 +543,9 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute return enabledRoutes, disabledRoutes } - seenRoute := make(map[route.ID]struct{}) + seenRoute := make(map[string]struct{}) - takeRoute := func(r *route.Route) { + takeRoute := func(r *nmdata.Route) { if _, ok := seenRoute[r.ID]; ok { return } @@ -565,7 +564,7 @@ func (c *NetworkMapComponents) getRoutingPeerRoutes(peerID string) (enabledRoute if entry.viaGroup { newPeerRoute := entry.route.Copy() newPeerRoute.PeerGroups = nil - newPeerRoute.ID = route.ID(string(entry.route.ID) + ":" + peerID) + newPeerRoute.ID = entry.route.ID + ":" + peerID takeRoute(newPeerRoute) continue } @@ -598,8 +597,8 @@ func (c *NetworkMapComponents) routesByPeer() map[string][]routeIndexEntry { return c.routesByPeerIdx } -func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, groupListMap LookupMap) []*route.Route { - var filteredRoutes []*route.Route +func (c *NetworkMapComponents) filterRoutesByGroups(routes []*nmdata.Route, groupListMap LookupMap) []*nmdata.Route { + var filteredRoutes []*nmdata.Route for _, r := range routes { for _, groupID := range r.Groups { _, found := groupListMap[groupID] @@ -612,8 +611,8 @@ func (c *NetworkMapComponents) filterRoutesByGroups(routes []*route.Route, group return filteredRoutes } -func (c *NetworkMapComponents) filterRoutesFromPeersOfSameHAGroup(routes []*route.Route, peerMemberships LookupMap) []*route.Route { - var filteredRoutes []*route.Route +func (c *NetworkMapComponents) filterRoutesFromPeersOfSameHAGroup(routes []*nmdata.Route, peerMemberships LookupMap) []*nmdata.Route { + var filteredRoutes []*nmdata.Route for _, r := range routes { _, found := peerMemberships[string(r.GetHAUniqueID())] if !found { @@ -646,7 +645,7 @@ func (c *NetworkMapComponents) getPeerRoutesFirewallRules(ctx context.Context, p return routesFirewallRules } -func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool) []*RouteFirewallRule { +func (c *NetworkMapComponents) getDefaultPermit(r *nmdata.Route, includeIPv6 bool) []*RouteFirewallRule { if r.Network.Addr().Is6() && !includeIPv6 { return nil } @@ -663,7 +662,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool Protocol: string(PolicyRuleProtocolALL), Domains: r.Domains, IsDynamic: r.IsDynamic(), - RouteID: r.ID, + RouteID: route.ID(r.ID), } rules := []*RouteFirewallRule{&rule} @@ -674,7 +673,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool ruleV6.SourceRanges = []string{"::/0"} if isDefaultV4 { ruleV6.Destination = "::/0" - ruleV6.RouteID = r.ID + "-v6-default" + ruleV6.RouteID = route.ID(r.ID + "-v6-default") } rules = append(rules, &ruleV6) } @@ -682,7 +681,7 @@ func (c *NetworkMapComponents) getDefaultPermit(r *route.Route, includeIPv6 bool return rules } -func (c *NetworkMapComponents) getDistributionGroupsPeers(r *route.Route) map[string]struct{} { +func (c *NetworkMapComponents) getDistributionGroupsPeers(r *nmdata.Route) map[string]struct{} { distPeers := make(map[string]struct{}) for _, id := range r.Groups { group := c.GetGroupInfo(id) @@ -697,8 +696,8 @@ func (c *NetworkMapComponents) getDistributionGroupsPeers(r *route.Route) map[st return distPeers } -func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups []string) []*Policy { - routePolicies := make([]*Policy, 0) +func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups []string) []*nmdata.Policy { + routePolicies := make([]*nmdata.Policy, 0) for _, groupID := range accessControlGroups { for _, policy := range c.Policies { for _, rule := range policy.Rules { @@ -712,7 +711,7 @@ func (c *NetworkMapComponents) getAllRoutePoliciesFromGroups(accessControlGroups return routePolicies } -func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID string, policies []*Policy, route *route.Route, distributionPeers map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule { +func (c *NetworkMapComponents) getRouteFirewallRules(ctx context.Context, peerID string, policies []*nmdata.Policy, route *nmdata.Route, distributionPeers map[string]struct{}, includeIPv6 bool) []*RouteFirewallRule { var fwRules []*RouteFirewallRule for _, policy := range policies { if !policy.Enabled { @@ -732,7 +731,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 *nmdata.PolicyRule, postureChecks []string, peerID string, distributionPeers map[string]struct{}) []*nmdata.Peer { distPeersWithPolicy := make(map[string]struct{}) for _, id := range rule.Sources { group := c.GetGroupInfo(id) @@ -751,7 +750,7 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st } } } - if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" { + if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" { _, distPeer := distributionPeers[rule.SourceResource.ID] _, valid := c.Peers[rule.SourceResource.ID] if distPeer && valid && c.ValidatePostureChecksOnPeer(rule.SourceResource.ID, postureChecks) { @@ -759,7 +758,7 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st } } - distributionGroupPeers := make([]*nbpeer.Peer, 0, len(distPeersWithPolicy)) + distributionGroupPeers := make([]*nmdata.Peer, 0, len(distPeersWithPolicy)) for pID := range distPeersWithPolicy { peerInfo := c.GetPeerInfo(pID) if peerInfo == nil { @@ -770,9 +769,9 @@ func (c *NetworkMapComponents) getRulePeers(rule *PolicyRule, postureChecks []st return distributionGroupPeers } -func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (bool, []*route.Route, map[string]struct{}) { +func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (bool, []*nmdata.Route, map[string]struct{}) { var isRoutingPeer bool - var routes []*route.Route + var routes []*nmdata.Route allSourcePeers := make(map[string]struct{}) for _, resource := range c.NetworkResources { @@ -799,12 +798,12 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutesToSync(peerID string) (b func (c *NetworkMapComponents) processResourcePolicies( peerID string, - resource *resourceTypes.NetworkResource, - networkRoutingPeers map[string]*routerTypes.NetworkRouter, + resource *nmdata.NetworkResource, + networkRoutingPeers map[string]*nmdata.NetworkRouter, addSourcePeers bool, allSourcePeers map[string]struct{}, -) []*route.Route { - var routes []*route.Route +) []*nmdata.Route { + var routes []*nmdata.Route for _, policy := range c.ResourcePoliciesMap[resource.ID] { peers := c.getResourcePolicyPeers(policy) @@ -826,17 +825,17 @@ func (c *NetworkMapComponents) processResourcePolicies( return routes } -func (c *NetworkMapComponents) getResourcePolicyPeers(policy *Policy) []string { - if policy.Rules[0].SourceResource.Type == ResourceTypePeer && policy.Rules[0].SourceResource.ID != "" { +func (c *NetworkMapComponents) getResourcePolicyPeers(policy *nmdata.Policy) []string { + if policy.Rules[0].SourceResource.Type == string(ResourceTypePeer) && policy.Rules[0].SourceResource.ID != "" { return []string{policy.Rules[0].SourceResource.ID} } return c.getUniquePeerIDsFromGroupsIDs(policy.SourceGroups()) } -func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *resourceTypes.NetworkResource, peerID string, router *routerTypes.NetworkRouter) []*route.Route { +func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *nmdata.NetworkResource, peerID string, router *nmdata.NetworkRouter) []*nmdata.Route { resourceAppliedPolicies := c.ResourcePoliciesMap[resource.ID] - var routes []*route.Route + var routes []*nmdata.Route if len(resourceAppliedPolicies) > 0 { peerInfo := c.GetPeerInfo(peerID) if peerInfo != nil { @@ -847,9 +846,9 @@ func (c *NetworkMapComponents) getNetworkResourcesRoutes(resource *resourceTypes return routes } -func (c *NetworkMapComponents) networkResourceToRoute(resource *resourceTypes.NetworkResource, peer *nbpeer.Peer, router *routerTypes.NetworkRouter) *route.Route { - r := &route.Route{ - ID: route.ID(resource.ID + ":" + peer.ID), +func (c *NetworkMapComponents) networkResourceToRoute(resource *nmdata.NetworkResource, peer *nmdata.Peer, router *nmdata.NetworkRouter) *nmdata.Route { + r := &nmdata.Route{ + ID: resource.ID + ":" + peer.ID, AccountID: resource.AccountID, Peer: peer.Key, PeerID: peer.ID, @@ -857,24 +856,24 @@ func (c *NetworkMapComponents) networkResourceToRoute(resource *resourceTypes.Ne Masquerade: router.Masquerade, Enabled: resource.Enabled, KeepRoute: true, - NetID: route.NetID(resource.Name), + NetID: resource.Name, Description: resource.Description, } - if resource.Type == resourceTypes.Host || resource.Type == resourceTypes.Subnet { + if resource.Type == string(resourceTypes.Host) || resource.Type == string(resourceTypes.Subnet) { r.Network = resource.Prefix - r.NetworkType = route.IPv4Network + r.NetworkType = nmdata.NetworkTypeIPv4 if resource.Prefix.Addr().Is6() { - r.NetworkType = route.IPv6Network + r.NetworkType = nmdata.NetworkTypeIPv6 } } - if resource.Type == resourceTypes.Domain { + if resource.Type == string(resourceTypes.Domain) { domainList, err := domain.FromStringList([]string{resource.Domain}) if err == nil { r.Domains = domainList - r.NetworkType = route.DomainNetwork + r.NetworkType = nmdata.NetworkTypeDomain r.Network = netip.PrefixFrom(netip.AddrFrom4([4]byte{192, 0, 2, 0}), 32) } } @@ -892,7 +891,7 @@ func (c *NetworkMapComponents) getPostureValidPeers(inputPeers []string, posture return dest } -func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*route.Route, includeIPv6 bool) []*RouteFirewallRule { +func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.Context, peerID string, routes []*nmdata.Route, includeIPv6 bool) []*RouteFirewallRule { routesFirewallRules := make([]*RouteFirewallRule, 0) peerInfo := c.GetPeerInfo(peerID) @@ -920,7 +919,7 @@ func (c *NetworkMapComponents) getPeerNetworkResourceFirewallRules(ctx context.C return routesFirewallRules } -func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[string]struct{} { +func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*nmdata.Policy) map[string]struct{} { sourcePeers := make(map[string]struct{}) for _, policy := range policies { @@ -936,7 +935,7 @@ func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[st } } - if rule.SourceResource.Type == ResourceTypePeer && rule.SourceResource.ID != "" { + if rule.SourceResource.Type == string(ResourceTypePeer) && rule.SourceResource.ID != "" { sourcePeers[rule.SourceResource.ID] = struct{}{} } } @@ -946,13 +945,13 @@ func (c *NetworkMapComponents) getPoliciesSourcePeers(policies []*Policy) map[st } func (c *NetworkMapComponents) addNetworksRoutingPeers( - networkResourcesRoutes []*route.Route, + networkResourcesRoutes []*nmdata.Route, peerID string, - peersToConnect []*nbpeer.Peer, - expiredPeers []*nbpeer.Peer, + peersToConnect []*nmdata.Peer, + expiredPeers []*nmdata.Peer, isRouter bool, sourcePeers map[string]struct{}, -) []*nbpeer.Peer { +) []*nmdata.Peer { networkRoutesPeers := make(map[string]struct{}, len(networkResourcesRoutes)) for _, r := range networkResourcesRoutes { @@ -1002,7 +1001,7 @@ type FirewallRuleContext struct { PortsJoined string } -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 *nmdata.Peer, rule *nmdata.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..b45bc3e40 100644 --- a/shared/management/types/networkmap_components_compact.go +++ b/shared/management/types/networkmap_components_compact.go @@ -1,11 +1,7 @@ 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" + nmdata "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" ) type GroupCompact struct { @@ -16,26 +12,26 @@ type GroupCompact struct { type NetworkMapComponentsCompact struct { PeerID string - Network *Network - AccountSettings *AccountSettingsInfo - DNSSettings *DNSSettings + Network *nmdata.Network + AccountSettings *nmdata.AccountSettingsInfo + DNSSettings *nmdata.DNSSettings CustomZoneDomain string - AllPeers []*nbpeer.Peer + AllPeers []*nmdata.Peer PeerIndexes []int RouterPeerIndexes []int Groups map[string]*GroupCompact - AllPolicies []*Policy + AllPolicies []*nmdata.Policy PolicyIndexes []int ResourcePoliciesMap map[string][]int - Routes []*route.Route - NameServerGroups []*nbdns.NameServerGroup - AllDNSRecords []nbdns.SimpleRecord - AccountZones []nbdns.CustomZone + Routes []*nmdata.Route + NameServerGroups []*nmdata.NameServerGroup + AllDNSRecords []nmdata.SimpleRecord + AccountZones []nmdata.CustomZone - RoutersMap map[string]map[string]*routerTypes.NetworkRouter - NetworkResources []*resourceTypes.NetworkResource + RoutersMap map[string]map[string]*nmdata.NetworkRouter + NetworkResources []*nmdata.NetworkResource GroupIDToUserIDs map[string][]string AllowedUserIDs map[string]struct{} @@ -44,7 +40,7 @@ type NetworkMapComponentsCompact struct { func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact { peerToIndex := make(map[string]int) - var allPeers []*nbpeer.Peer + var allPeers []*nmdata.Peer for id, peer := range c.Peers { if _, exists := peerToIndex[id]; !exists { @@ -84,8 +80,8 @@ func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact { } } - policyToIndex := make(map[*Policy]int) - var allPolicies []*Policy + policyToIndex := make(map[*nmdata.Policy]int) + var allPolicies []*nmdata.Policy for _, policy := range c.Policies { if _, exists := policyToIndex[policy]; !exists { @@ -150,7 +146,7 @@ func (c *NetworkMapComponents) ToCompact() *NetworkMapComponentsCompact { } func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents { - peers := make(map[string]*nbpeer.Peer, len(c.PeerIndexes)) + peers := make(map[string]*nmdata.Peer, len(c.PeerIndexes)) for _, idx := range c.PeerIndexes { if idx >= 0 && idx < len(c.AllPeers) { peer := c.AllPeers[idx] @@ -158,7 +154,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents { } } - routerPeers := make(map[string]*nbpeer.Peer, len(c.RouterPeerIndexes)) + routerPeers := make(map[string]*nmdata.Peer, len(c.RouterPeerIndexes)) for _, idx := range c.RouterPeerIndexes { if idx >= 0 && idx < len(c.AllPeers) { peer := c.AllPeers[idx] @@ -166,7 +162,7 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents { } } - groups := make(map[string]*Group, len(c.Groups)) + groups := make(map[string]*nmdata.Group, len(c.Groups)) for id, gc := range c.Groups { peerIDs := make([]string, 0, len(gc.PeerIndexes)) for _, idx := range gc.PeerIndexes { @@ -174,25 +170,24 @@ func (c *NetworkMapComponentsCompact) ToFull() *NetworkMapComponents { peerIDs = append(peerIDs, c.AllPeers[idx].ID) } } - groups[id] = &Group{ - ID: id, + groups[id] = &nmdata.Group{ Name: gc.Name, Peers: peerIDs, } } - policies := make([]*Policy, len(c.PolicyIndexes)) + policies := make([]*nmdata.Policy, len(c.PolicyIndexes)) for i, idx := range c.PolicyIndexes { if idx >= 0 && idx < len(c.AllPolicies) { policies[i] = c.AllPolicies[idx] } } - var resourcePoliciesMap map[string][]*Policy + var resourcePoliciesMap map[string][]*nmdata.Policy if len(c.ResourcePoliciesMap) > 0 { - resourcePoliciesMap = make(map[string][]*Policy, len(c.ResourcePoliciesMap)) + resourcePoliciesMap = make(map[string][]*nmdata.Policy, len(c.ResourcePoliciesMap)) for resID, indexes := range c.ResourcePoliciesMap { - pols := make([]*Policy, 0, len(indexes)) + pols := make([]*nmdata.Policy, 0, len(indexes)) for _, idx := range indexes { if idx >= 0 && idx < len(c.AllPolicies) { pols = append(pols, c.AllPolicies[idx]) diff --git a/shared/management/types/nmdata_convert.go b/shared/management/types/nmdata_convert.go new file mode 100644 index 000000000..2a7998773 --- /dev/null +++ b/shared/management/types/nmdata_convert.go @@ -0,0 +1,70 @@ +package types + +import ( + nbdns "github.com/netbirdio/netbird/dns" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" +) + +// This file holds the twin→real converters that survive the twin-NetworkMap +// refactor: only the DNS materialization. NetworkMap.DNSConfig stays a real +// nbdns.Config (the client DNS type), so Calculate converts the twin DNS +// components to nbdns at the output boundary. Peers/Routes/Network flow as +// twins all the way through and need no conversion. + +func toRealNSGroup(n *nmdata.NameServerGroup) *nbdns.NameServerGroup { + if n == nil { + return nil + } + nameServers := make([]nbdns.NameServer, 0, len(n.NameServers)) + for _, ns := range n.NameServers { + nameServers = append(nameServers, nbdns.NameServer{ + IP: ns.IP, + NSType: nbdns.NameServerType(ns.NSType), + Port: ns.Port, + }) + } + return &nbdns.NameServerGroup{ + ID: n.ID, + Name: n.Name, + Description: n.Description, + NameServers: nameServers, + Groups: n.Groups, + Primary: n.Primary, + Domains: n.Domains, + Enabled: n.Enabled, + SearchDomainsEnabled: n.SearchDomainsEnabled, + } +} + +func toRealRecords(recs []nmdata.SimpleRecord) []nbdns.SimpleRecord { + if recs == nil { + return nil + } + out := make([]nbdns.SimpleRecord, len(recs)) + for i, r := range recs { + out[i] = nbdns.SimpleRecord{ + Name: r.Name, + Type: r.Type, + Class: r.Class, + TTL: r.TTL, + RData: r.RData, + } + } + return out +} + +func toRealZones(zones []nmdata.CustomZone) []nbdns.CustomZone { + if zones == nil { + return nil + } + out := make([]nbdns.CustomZone, len(zones)) + for i, z := range zones { + out[i] = nbdns.CustomZone{ + Domain: z.Domain, + Records: toRealRecords(z.Records), + SearchDomainDisabled: z.SearchDomainDisabled, + NonAuthoritative: z.NonAuthoritative, + } + } + return out +}