From fd23461b03fbecaa77ad619b33cfb4833e225c25 Mon Sep 17 00:00:00 2001 From: pascal Date: Thu, 6 Aug 2026 15:57:43 +0200 Subject: [PATCH] add network map compute test --- .../networkmap/networkmapcompute_test.go | 1489 +++++++++++++++++ 1 file changed, 1489 insertions(+) create mode 100644 shared/management/networkmap/networkmapcompute_test.go diff --git a/shared/management/networkmap/networkmapcompute_test.go b/shared/management/networkmap/networkmapcompute_test.go new file mode 100644 index 000000000..e9500c4aa --- /dev/null +++ b/shared/management/networkmap/networkmapcompute_test.go @@ -0,0 +1,1489 @@ +package networkmap_test + +import ( + "context" + "fmt" + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/netbirdio/netbird/shared/management/networkmap" + "github.com/netbirdio/netbird/shared/management/networkmap/nmdata" + nbtypes "github.com/netbirdio/netbird/shared/management/types" +) + +const ( + targetID = "peer-target" + postureMinVersion = "0.30.0" + passingVersion = "1.0.0" + failingVersion = "0.1.0" +) + +func newPeer(id string, hostNum byte) *nmdata.Peer { + return &nmdata.Peer{ + ID: id, + Key: "key-" + id, + IP: netip.AddrFrom4([4]byte{100, 64, 0, hostNum}), + DNSLabel: id, + Meta: nmdata.PeerSystemMeta{WtVersion: passingVersion}, + } +} + +func newNMD(peers ...*nmdata.Peer) *networkmap.NetworkMapData { + nmd := &networkmap.NetworkMapData{ + Peers: make(map[string]*nmdata.Peer), + Groups: make(map[string]*nmdata.Group), + ValidatedPeers: make(map[string]struct{}), + Network: &nmdata.Network{Identifier: "network-1", Serial: 7}, + AccountSettings: &nmdata.AccountSettingsInfo{}, + DNSSettings: &nmdata.DNSSettings{}, + } + for _, p := range peers { + nmd.Peers[p.ID] = p + nmd.ValidatedPeers[p.ID] = struct{}{} + } + return nmd +} + +func addGroup(nmd *networkmap.NetworkMapData, id string, peerIDs ...string) *nmdata.Group { + g := &nmdata.Group{ID: id, Name: id, Peers: peerIDs} + nmd.Groups[id] = g + return g +} + +func newRule(sources, destinations []string) *nmdata.PolicyRule { + return &nmdata.PolicyRule{ + Enabled: true, + Action: string(nbtypes.PolicyTrafficActionAccept), + Protocol: string(nbtypes.PolicyRuleProtocolTCP), + Bidirectional: true, + Sources: sources, + Destinations: destinations, + } +} + +func newPolicy(id string, rules ...*nmdata.PolicyRule) *nmdata.Policy { + for i, r := range rules { + if r.ID == "" { + r.ID = fmt.Sprintf("%s-rule-%d", id, i) + } + r.PolicyID = id + } + return &nmdata.Policy{ID: id, Enabled: true, Rules: rules} +} + +func addVersionCheck(nmd *networkmap.NetworkMapData, id, minVersion string) { + if nmd.PostureChecks == nil { + nmd.PostureChecks = make(map[string]*nmdata.PostureChecks) + } + nmd.PostureChecks[id] = &nmdata.PostureChecks{ + ID: id, + Checks: nmdata.ChecksDefinition{NBVersionCheck: &nmdata.NBVersionCheck{MinVersion: minVersion}}, + } +} + +func compute(nmd *networkmap.NetworkMapData, peerID string) *nbtypes.NetworkMapComponents { + return nmd.GetPeerNetworkMapComponents(peerID, nmdata.CustomZone{}) +} + +func peerIDSet(peers map[string]*nmdata.Peer) []string { + ids := make([]string, 0, len(peers)) + for id := range peers { + ids = append(ids, id) + } + return ids +} + +func policyIDs(policies []*nmdata.Policy) []string { + ids := make([]string, 0, len(policies)) + for _, p := range policies { + ids = append(ids, p.ID) + } + return ids +} + +func groupIDSet(groups map[string]*nmdata.Group) []string { + ids := make([]string, 0, len(groups)) + for id := range groups { + ids = append(ids, id) + } + return ids +} + +func TestGetPeerNetworkMapComponents_UnknownPeer(t *testing.T) { + nmd := newNMD(newPeer("peer-a", 2)) + + c := compute(nmd, "missing") + + require.True(t, c.IsEmpty()) + assert.Equal(t, "missing", c.PeerID) + assert.Same(t, nmd.Network, c.Network) + require.Contains(t, c.Peers, "missing") + assert.Nil(t, c.Peers["missing"]) + assert.Len(t, c.Peers, 1) + assert.Nil(t, c.AccountSettings) + assert.Nil(t, c.Policies) + assert.False(t, c.ForceRoutingPeerDNSResolution) +} + +func TestGetPeerNetworkMapComponents_UnvalidatedPeer(t *testing.T) { + target := newPeer(targetID, 1) + nmd := newNMD(target) + delete(nmd.ValidatedPeers, targetID) + + c := compute(nmd, targetID) + + require.True(t, c.IsEmpty()) + assert.Equal(t, targetID, c.PeerID) + assert.Same(t, target, c.Peers[targetID]) + assert.Len(t, c.Peers, 1) + assert.Nil(t, c.AccountSettings) + assert.Nil(t, c.Groups) +} + +// The forced-DNS flag must be computed even on the empty-components early +// exits, so an unknown or unvalidated proxy routing peer still starts its DNS +// forwarder. +func TestGetPeerNetworkMapComponents_EmptyComponentsKeepForcedDNSResolution(t *testing.T) { + build := func() *networkmap.NetworkMapData { + nmd := newNMD(newPeer("unval-router", 1)) + delete(nmd.ValidatedPeers, "unval-router") + nmd.NetworkResources = []*nmdata.NetworkResource{ + {ID: "res-1", NetworkID: "net-1", Type: string(nbtypes.ResourceTypeDomain), Enabled: true}, + } + nmd.ProxyTargetedDomainResourceIDs = map[string]struct{}{"res-1": {}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {"ghost-router": {}, "unval-router": {}}} + return nmd + } + + t.Run("unknown peer", func(t *testing.T) { + c := compute(build(), "ghost-router") + require.True(t, c.IsEmpty()) + assert.True(t, c.ForceRoutingPeerDNSResolution) + }) + + t.Run("unvalidated peer", func(t *testing.T) { + c := compute(build(), "unval-router") + require.True(t, c.IsEmpty()) + assert.True(t, c.ForceRoutingPeerDNSResolution) + }) +} + +func TestGetPeerNetworkMapComponents_ForceRoutingPeerDNSResolution(t *testing.T) { + forced := func(mutate func(*networkmap.NetworkMapData)) bool { + nmd := newNMD(newPeer(targetID, 1)) + nmd.NetworkResources = []*nmdata.NetworkResource{ + {ID: "res-1", NetworkID: "net-1", Type: string(nbtypes.ResourceTypeDomain), Enabled: true}, + } + nmd.ProxyTargetedDomainResourceIDs = map[string]struct{}{"res-1": {}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {targetID: {}}} + if mutate != nil { + mutate(nmd) + } + return compute(nmd, targetID).ForceRoutingPeerDNSResolution + } + + t.Run("router of targeted domain resource is forced", func(t *testing.T) { + assert.True(t, forced(nil)) + }) + t.Run("no proxy-targeted resources", func(t *testing.T) { + assert.False(t, forced(func(nmd *networkmap.NetworkMapData) { + nmd.ProxyTargetedDomainResourceIDs = nil + })) + }) + t.Run("resource disabled", func(t *testing.T) { + assert.False(t, forced(func(nmd *networkmap.NetworkMapData) { + nmd.NetworkResources[0].Enabled = false + })) + }) + t.Run("resource not a domain", func(t *testing.T) { + assert.False(t, forced(func(nmd *networkmap.NetworkMapData) { + nmd.NetworkResources[0].Type = string(nbtypes.ResourceTypeHost) + })) + }) + t.Run("resource not targeted", func(t *testing.T) { + assert.False(t, forced(func(nmd *networkmap.NetworkMapData) { + nmd.ProxyTargetedDomainResourceIDs = map[string]struct{}{"res-other": {}} + })) + }) + t.Run("peer not a router of the resource network", func(t *testing.T) { + assert.False(t, forced(func(nmd *networkmap.NetworkMapData) { + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {"someone-else": {}}} + })) + }) + t.Run("nil resource entry tolerated", func(t *testing.T) { + assert.True(t, forced(func(nmd *networkmap.NetworkMapData) { + nmd.NetworkResources = append([]*nmdata.NetworkResource{nil}, nmd.NetworkResources...) + })) + }) +} + +func TestGetPeerNetworkMapComponents_CoreFieldsPassThrough(t *testing.T) { + target := newPeer(targetID, 1) + nmd := newNMD(target) + nmd.NetworkXIDToPublicID = map[string]string{"net-xid": "net-pub"} + nmd.PostureCheckXIDToPublicID = map[string]string{"pc-xid": "pc-pub"} + + c := nmd.GetPeerNetworkMapComponents(targetID, nmdata.CustomZone{Domain: "acme.netbird.cloud."}) + + require.False(t, c.IsEmpty()) + assert.Equal(t, targetID, c.PeerID) + assert.Same(t, nmd.Network, c.Network) + assert.Same(t, nmd.AccountSettings, c.AccountSettings) + assert.Same(t, nmd.DNSSettings, c.DNSSettings) + assert.Equal(t, "acme.netbird.cloud.", c.CustomZoneDomain) + assert.Equal(t, nmd.NetworkXIDToPublicID, c.NetworkXIDToPublicID) + assert.Equal(t, nmd.PostureCheckXIDToPublicID, c.PostureCheckXIDToPublicID) + + assert.Equal(t, map[string]*nmdata.Peer{targetID: target}, c.Peers) + assert.Empty(t, c.Groups) + assert.Empty(t, c.Policies) + assert.Empty(t, c.Routes) + assert.Empty(t, c.NameServerGroups) + assert.Empty(t, c.NetworkResources) + assert.Empty(t, c.ResourcePoliciesMap) + assert.Empty(t, c.RoutersMap) + assert.Empty(t, c.RouterPeers) + assert.Empty(t, c.PostureFailedPeers) + assert.Nil(t, c.AllDNSRecords) + assert.Empty(t, c.AccountZones) + assert.Nil(t, c.GroupIDToUserIDs) + assert.Nil(t, c.AllowedUserIDs) + assert.False(t, c.ForceRoutingPeerDNSResolution) +} + +func TestGetPeerNetworkMapComponents_OwnGroupsTrimmedWithoutMutatingStore(t *testing.T) { + target := newPeer(targetID, 1) + bystander := newPeer("peer-bystander", 2) + nmd := newNMD(target, bystander) + stored := addGroup(nmd, "g-mixed", targetID, bystander.ID) + + c := compute(nmd, targetID) + + require.Contains(t, c.Groups, "g-mixed") + assert.Equal(t, []string{targetID}, c.Groups["g-mixed"].Peers) + assert.NotSame(t, stored, c.Groups["g-mixed"]) + assert.Equal(t, []string{targetID, bystander.ID}, stored.Peers) +} + +func TestGetPeerNetworkMapComponents_PolicyRelevance(t *testing.T) { + t.Run("peer in sources pulls destination peers and groups", func(t *testing.T) { + target := newPeer(targetID, 1) + srcSibling := newPeer("peer-src-sibling", 2) + dst := newPeer("peer-dst", 3) + nmd := newNMD(target, srcSibling, dst) + addGroup(nmd, "g-src", targetID, srcSibling.ID) + addGroup(nmd, "g-dst", dst.ID) + p := newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"})) + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, dst.ID}, peerIDSet(c.Peers), + "source-side siblings must not be connected") + assert.ElementsMatch(t, []string{"g-src", "g-dst"}, groupIDSet(c.Groups)) + assert.Equal(t, []string{targetID}, c.Groups["g-src"].Peers) + assert.Equal(t, []string{dst.ID}, c.Groups["g-dst"].Peers) + }) + + t.Run("peer in destinations pulls source peers and groups", func(t *testing.T) { + target := newPeer(targetID, 1) + src := newPeer("peer-src", 2) + nmd := newNMD(target, src) + addGroup(nmd, "g-src", src.ID) + addGroup(nmd, "g-dst", targetID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"}))} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, src.ID}, peerIDSet(c.Peers)) + assert.ElementsMatch(t, []string{"g-src", "g-dst"}, groupIDSet(c.Groups)) + }) + + t.Run("unrelated policy contributes nothing", func(t *testing.T) { + target := newPeer(targetID, 1) + a := newPeer("peer-a", 2) + b := newPeer("peer-b", 3) + nmd := newNMD(target, a, b) + addGroup(nmd, "g-own", targetID) + addGroup(nmd, "g-a", a.ID) + addGroup(nmd, "g-b", b.ID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", newRule([]string{"g-a"}, []string{"g-b"}))} + + c := compute(nmd, targetID) + + assert.Empty(t, c.Policies) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + assert.ElementsMatch(t, []string{"g-own"}, groupIDSet(c.Groups)) + }) + + t.Run("disabled policy ignored", func(t *testing.T) { + target := newPeer(targetID, 1) + src := newPeer("peer-src", 2) + nmd := newNMD(target, src) + addGroup(nmd, "g-src", src.ID) + addGroup(nmd, "g-dst", targetID) + p := newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"})) + p.Enabled = false + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.Empty(t, c.Policies) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) + + t.Run("disabled rule ignored", func(t *testing.T) { + target := newPeer(targetID, 1) + src := newPeer("peer-src", 2) + nmd := newNMD(target, src) + addGroup(nmd, "g-src", src.ID) + addGroup(nmd, "g-dst", targetID) + rule := newRule([]string{"g-src"}, []string{"g-dst"}) + rule.Enabled = false + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", rule)} + + c := compute(nmd, targetID) + + assert.Empty(t, c.Policies) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) + + t.Run("peer on both sides pulls peers from both directions", func(t *testing.T) { + target := newPeer(targetID, 1) + x := newPeer("peer-x", 2) + y := newPeer("peer-y", 3) + nmd := newNMD(target, x, y) + addGroup(nmd, "g-src", targetID, x.ID) + addGroup(nmd, "g-dst", targetID, y.ID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"}))} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, x.ID, y.ID}, peerIDSet(c.Peers), + "both the source-side and destination-side counterparts must connect") + }) + + t.Run("rule referencing missing group tolerated", func(t *testing.T) { + target := newPeer(targetID, 1) + nmd := newNMD(target) + addGroup(nmd, "g-dst", targetID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", newRule([]string{"g-ghost"}, []string{"g-dst"}))} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) + + t.Run("nil policy and rule entries tolerated", func(t *testing.T) { + target := newPeer(targetID, 1) + dst := newPeer("peer-dst", 2) + nmd := newNMD(target, dst) + addGroup(nmd, "g-src", targetID) + addGroup(nmd, "g-dst", dst.ID) + p := newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"})) + p.Rules = append([]*nmdata.PolicyRule{nil}, p.Rules...) + nmd.Policies = []*nmdata.Policy{nil, p} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, dst.ID}, peerIDSet(c.Peers)) + }) +} + +func TestGetPeerNetworkMapComponents_PeerResourceRules(t *testing.T) { + peerResource := func(id string) nmdata.Resource { + return nmdata.Resource{ID: id, Type: string(nbtypes.ResourceTypePeer)} + } + + t.Run("target as source resource", func(t *testing.T) { + target := newPeer(targetID, 1) + dst := newPeer("peer-dst", 2) + nmd := newNMD(target, dst) + addGroup(nmd, "g-dst", dst.ID) + rule := newRule(nil, []string{"g-dst"}) + rule.SourceResource = peerResource(targetID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", rule)} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, dst.ID}, peerIDSet(c.Peers)) + }) + + t.Run("target as destination resource", func(t *testing.T) { + target := newPeer(targetID, 1) + src := newPeer("peer-src", 2) + nmd := newNMD(target, src) + addGroup(nmd, "g-src", src.ID) + rule := newRule([]string{"g-src"}, nil) + rule.DestinationResource = peerResource(targetID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", rule)} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, src.ID}, peerIDSet(c.Peers)) + }) + + t.Run("remote peer as destination resource", func(t *testing.T) { + target := newPeer(targetID, 1) + remote := newPeer("peer-remote", 2) + nmd := newNMD(target, remote) + addGroup(nmd, "g-src", targetID) + rule := newRule([]string{"g-src"}, nil) + rule.DestinationResource = peerResource(remote.ID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", rule)} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, remote.ID}, peerIDSet(c.Peers)) + }) + + // Legacy parity: directly referenced peers bypass the ValidatedPeers gate + // and posture checks that group-derived peers go through; the client-side + // Calculate shares this behavior via getPeerFromResource. + t.Run("unvalidated source resource peer still connects", func(t *testing.T) { + target := newPeer(targetID, 1) + unval := newPeer("peer-unval", 2) + nmd := newNMD(target, unval) + delete(nmd.ValidatedPeers, unval.ID) + addGroup(nmd, "g-dst", targetID) + rule := newRule(nil, []string{"g-dst"}) + rule.SourceResource = peerResource(unval.ID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", rule)} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, unval.ID}, peerIDSet(c.Peers)) + }) + + t.Run("source resource peer bypasses posture checks", func(t *testing.T) { + target := newPeer(targetID, 1) + failer := newPeer("peer-failer", 2) + failer.Meta.WtVersion = failingVersion + nmd := newNMD(target, failer) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-dst", targetID) + rule := newRule(nil, []string{"g-dst"}) + rule.SourceResource = peerResource(failer.ID) + p := newPolicy("p-1", rule) + p.SourcePostureChecks = []string{"pc-1"} + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, failer.ID}, peerIDSet(c.Peers)) + assert.Empty(t, c.PostureFailedPeers) + }) + + t.Run("unrelated peer resource rule ignored", func(t *testing.T) { + target := newPeer(targetID, 1) + a := newPeer("peer-a", 2) + b := newPeer("peer-b", 3) + nmd := newNMD(target, a, b) + rule := newRule(nil, nil) + rule.SourceResource = peerResource(a.ID) + rule.DestinationResource = peerResource(b.ID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", rule)} + + c := compute(nmd, targetID) + + assert.Empty(t, c.Policies) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) +} + +// A destination list containing a group named "All" short-circuits peer +// expansion to that group alone, dropping peers accumulated from earlier +// groups. Groups themselves are still all shipped. Mirrors legacy behavior +// that the wire encoding depends on (see +// TestEnvelopeRoundTrip_AllGroupShortCircuitParity). +func TestGetPeerNetworkMapComponents_AllGroupShortCircuit(t *testing.T) { + target := newPeer(targetID, 1) + first := newPeer("peer-first", 2) + allMember := newPeer("peer-all-member", 3) + nmd := newNMD(target, first, allMember) + addGroup(nmd, "g-src", targetID) + addGroup(nmd, "g-first", first.ID) + nmd.Groups["g-all"] = &nmdata.Group{ID: "g-all", Name: nmdata.GroupAllName, Peers: []string{targetID, allMember.ID}} + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-first", "g-all"}))} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, allMember.ID}, peerIDSet(c.Peers), + "peers from groups before the All group must be dropped by the short-circuit") + assert.ElementsMatch(t, []string{"g-src", "g-first", "g-all"}, groupIDSet(c.Groups)) + assert.Empty(t, c.Groups["g-first"].Peers) +} + +func TestGetPeerNetworkMapComponents_UnvalidatedPolicyPeersExcluded(t *testing.T) { + target := newPeer(targetID, 1) + srcOK := newPeer("peer-src-ok", 2) + srcUnval := newPeer("peer-src-unval", 3) + nmd := newNMD(target, srcOK, srcUnval) + delete(nmd.ValidatedPeers, srcUnval.ID) + addGroup(nmd, "g-src", srcOK.ID, srcUnval.ID, "peer-deleted") + addGroup(nmd, "g-dst", targetID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"}))} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, srcOK.ID}, peerIDSet(c.Peers), + "unvalidated and dangling group members must not connect") + assert.Equal(t, []string{srcOK.ID}, c.Groups["g-src"].Peers) +} + +// Multi-group rules take the union path of getPeersFromGroups (no All-group +// short-circuit); validation and source posture checks apply per member. +func TestGetPeerNetworkMapComponents_MultiGroupSources(t *testing.T) { + target := newPeer(targetID, 1) + dup := newPeer("peer-dup", 2) + unval := newPeer("peer-unval", 3) + failer := newPeer("peer-failer", 4) + failer.Meta.WtVersion = failingVersion + solo := newPeer("peer-solo", 5) + nmd := newNMD(target, dup, unval, failer, solo) + delete(nmd.ValidatedPeers, unval.ID) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-1", targetID, dup.ID, unval.ID, "peer-deleted") + addGroup(nmd, "g-2", dup.ID, failer.ID, solo.ID) + addGroup(nmd, "g-tgt", targetID) + p := newPolicy("p-1", newRule([]string{"g-1", "g-2"}, []string{"g-tgt"})) + p.SourcePostureChecks = []string{"pc-1"} + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, dup.ID, solo.ID}, peerIDSet(c.Peers)) + assert.Empty(t, c.PostureFailedPeers, + "failer is not otherwise connected, so its failure record is pruned") +} + +func TestGetPeerNetworkMapComponents_PostureChecks(t *testing.T) { + t.Run("failing source peer excluded without orphan failure record", func(t *testing.T) { + target := newPeer(targetID, 1) + failer := newPeer("peer-failer", 2) + failer.Meta.WtVersion = failingVersion + nmd := newNMD(target, failer) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-src", failer.ID) + addGroup(nmd, "g-dst", targetID) + p := newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"})) + p.SourcePostureChecks = []string{"pc-1"} + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + assert.Empty(t, c.PostureFailedPeers, + "failure records for peers absent from the map must be pruned") + }) + + t.Run("failure recorded when peer is connected via another policy", func(t *testing.T) { + target := newPeer(targetID, 1) + failer := newPeer("peer-failer", 2) + failer.Meta.WtVersion = failingVersion + nmd := newNMD(target, failer) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-src", failer.ID) + addGroup(nmd, "g-dst", targetID) + checked := newPolicy("p-checked", newRule([]string{"g-src"}, []string{"g-dst"})) + checked.SourcePostureChecks = []string{"pc-1"} + open := newPolicy("p-open", newRule([]string{"g-src"}, []string{"g-dst"})) + nmd.Policies = []*nmdata.Policy{checked, open} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, failer.ID}, peerIDSet(c.Peers)) + assert.Equal(t, map[string]map[string]struct{}{"pc-1": {failer.ID: {}}}, c.PostureFailedPeers) + }) + + t.Run("destination peers bypass source posture checks", func(t *testing.T) { + target := newPeer(targetID, 1) + failer := newPeer("peer-failer", 2) + failer.Meta.WtVersion = failingVersion + nmd := newNMD(target, failer) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-src", targetID) + addGroup(nmd, "g-dst", failer.ID) + p := newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"})) + p.SourcePostureChecks = []string{"pc-1"} + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, failer.ID}, peerIDSet(c.Peers)) + assert.Empty(t, c.PostureFailedPeers) + }) + + t.Run("target failing its own source check drops the policy", func(t *testing.T) { + target := newPeer(targetID, 1) + target.Meta.WtVersion = failingVersion + dst := newPeer("peer-dst", 2) + nmd := newNMD(target, dst) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-src", targetID) + addGroup(nmd, "g-dst", dst.ID) + p := newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"})) + p.SourcePostureChecks = []string{"pc-1"} + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.Empty(t, c.Policies) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + assert.Empty(t, c.PostureFailedPeers) + }) + + t.Run("failure keyed by the first failing check", func(t *testing.T) { + target := newPeer(targetID, 1) + failer := newPeer("peer-failer", 2) + nmd := newNMD(target, failer) + addVersionCheck(nmd, "pc-pass", postureMinVersion) + addVersionCheck(nmd, "pc-fail", "2.0.0") + addGroup(nmd, "g-src", failer.ID) + addGroup(nmd, "g-dst", targetID) + checked := newPolicy("p-checked", newRule([]string{"g-src"}, []string{"g-dst"})) + checked.SourcePostureChecks = []string{"pc-pass", "pc-fail"} + open := newPolicy("p-open", newRule([]string{"g-src"}, []string{"g-dst"})) + nmd.Policies = []*nmdata.Policy{checked, open} + + c := compute(nmd, targetID) + + assert.Equal(t, map[string]map[string]struct{}{"pc-fail": {failer.ID: {}}}, c.PostureFailedPeers, + "the record must be keyed by the failing check, not the first listed") + }) + + t.Run("unknown posture check id passes everyone", func(t *testing.T) { + target := newPeer(targetID, 1) + src := newPeer("peer-src", 2) + nmd := newNMD(target, src) + addGroup(nmd, "g-src", src.ID) + addGroup(nmd, "g-dst", targetID) + p := newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"})) + p.SourcePostureChecks = []string{"pc-ghost"} + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, src.ID}, peerIDSet(c.Peers)) + assert.Empty(t, c.PostureFailedPeers) + }) +} + +func TestGetPeerNetworkMapComponents_Routes(t *testing.T) { + t.Run("owned route relevant even when disabled", func(t *testing.T) { + target := newPeer(targetID, 1) + dist := newPeer("peer-dist", 2) + nmd := newNMD(target, dist) + addGroup(nmd, "g-dist", dist.ID) + addGroup(nmd, "g-acl") + r := &nmdata.Route{ID: "r-1", Peer: targetID, Enabled: false, Groups: []string{"g-dist"}, AccessControlGroups: []string{"g-acl"}} + nmd.Routes = []*nmdata.Route{r} + + c := compute(nmd, targetID) + + require.Len(t, c.Routes, 1) + assert.Same(t, r, c.Routes[0]) + assert.Contains(t, c.Groups, "g-dist") + assert.NotContains(t, c.Groups, "g-acl", + "access control groups of a disabled route must not be collected") + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers), + "distribution group members are not connected by the route itself") + }) + + t.Run("peer-group route disabled still ships and connects HA members", func(t *testing.T) { + target := newPeer(targetID, 1) + ha := newPeer("peer-ha", 2) + haUnval := newPeer("peer-ha-unval", 3) + nmd := newNMD(target, ha, haUnval) + delete(nmd.ValidatedPeers, haUnval.ID) + addGroup(nmd, "g-ha", targetID, ha.ID, haUnval.ID) + r := &nmdata.Route{ID: "r-1", PeerGroups: []string{"g-ha", "g-ghost"}, Enabled: false} + nmd.Routes = []*nmdata.Route{r} + + c := compute(nmd, targetID) + + require.Len(t, c.Routes, 1) + assert.ElementsMatch(t, []string{targetID, ha.ID}, peerIDSet(c.Peers)) + assert.Equal(t, []string{targetID, ha.ID}, c.Groups["g-ha"].Peers) + }) + + t.Run("route consumer connects HA routing peers from peer groups", func(t *testing.T) { + target := newPeer(targetID, 1) + router1 := newPeer("peer-router-1", 2) + router2 := newPeer("peer-router-2", 3) + routerUnval := newPeer("peer-router-unval", 4) + nmd := newNMD(target, router1, router2, routerUnval) + delete(nmd.ValidatedPeers, routerUnval.ID) + addGroup(nmd, "g-ha", router1.ID, router2.ID, routerUnval.ID) + addGroup(nmd, "g-dist", targetID) + nmd.Routes = []*nmdata.Route{{ID: "r-1", PeerGroups: []string{"g-ha"}, Groups: []string{"g-dist"}, Enabled: true}} + + c := compute(nmd, targetID) + + require.Len(t, c.Routes, 1) + assert.ElementsMatch(t, []string{targetID, router1.ID, router2.ID}, peerIDSet(c.Peers), + "the consumer must connect to every validated HA router") + assert.Equal(t, []string{router1.ID, router2.ID}, c.Groups["g-ha"].Peers) + }) + + t.Run("distribution route connects routing peer", func(t *testing.T) { + target := newPeer(targetID, 1) + router := newPeer("peer-router", 2) + nmd := newNMD(target, router) + addGroup(nmd, "g-dist", targetID) + r := &nmdata.Route{ID: "r-1", Peer: router.ID, Enabled: true, Groups: []string{"g-dist"}} + nmd.Routes = []*nmdata.Route{r} + + c := compute(nmd, targetID) + + require.Len(t, c.Routes, 1) + assert.ElementsMatch(t, []string{targetID, router.ID}, peerIDSet(c.Peers)) + }) + + t.Run("disabled distribution route not relevant", func(t *testing.T) { + target := newPeer(targetID, 1) + router := newPeer("peer-router", 2) + nmd := newNMD(target, router) + addGroup(nmd, "g-dist", targetID) + nmd.Routes = []*nmdata.Route{{ID: "r-1", Peer: router.ID, Enabled: false, Groups: []string{"g-dist"}}} + + c := compute(nmd, targetID) + + assert.Empty(t, c.Routes) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) + + t.Run("unvalidated routing peer excluded but route ships", func(t *testing.T) { + target := newPeer(targetID, 1) + router := newPeer("peer-router", 2) + nmd := newNMD(target, router) + delete(nmd.ValidatedPeers, router.ID) + addGroup(nmd, "g-dist", targetID) + nmd.Routes = []*nmdata.Route{{ID: "r-1", Peer: router.ID, Enabled: true, Groups: []string{"g-dist"}}} + + c := compute(nmd, targetID) + + require.Len(t, c.Routes, 1) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) + + t.Run("nil and unrelated routes skipped", func(t *testing.T) { + target := newPeer(targetID, 1) + other := newPeer("peer-other", 2) + nmd := newNMD(target, other) + addGroup(nmd, "g-dist", targetID) + addGroup(nmd, "g-foreign", other.ID) + owned := &nmdata.Route{ID: "r-owned", Peer: targetID, Enabled: true} + nmd.Routes = []*nmdata.Route{nil, {ID: "r-foreign", Peer: other.ID, Enabled: true, Groups: []string{"g-foreign"}}, owned} + + c := compute(nmd, targetID) + + require.Len(t, c.Routes, 1) + assert.Same(t, owned, c.Routes[0]) + }) +} + +// A policy whose destinations hit an enabled route's access control groups is +// shipped so the routing peer can build route firewall rules, but its peers +// are not connected through this bridge. +func TestGetPeerNetworkMapComponents_RouteAccessControlBridging(t *testing.T) { + t.Run("policy targeting route ACG becomes relevant", func(t *testing.T) { + target := newPeer(targetID, 1) + router := newPeer("peer-router", 2) + remote := newPeer("peer-remote", 3) + nmd := newNMD(target, router, remote) + addGroup(nmd, "g-dist", targetID) + addGroup(nmd, "g-acl") + addGroup(nmd, "g-remote", remote.ID) + nmd.Routes = []*nmdata.Route{{ID: "r-1", Peer: router.ID, Enabled: true, Groups: []string{"g-dist"}, AccessControlGroups: []string{"g-acl"}}} + nmd.Policies = []*nmdata.Policy{newPolicy("p-acl", newRule([]string{"g-remote"}, []string{"g-acl"}))} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-acl"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{"g-dist", "g-acl", "g-remote"}, groupIDSet(c.Groups)) + assert.ElementsMatch(t, []string{targetID, router.ID}, peerIDSet(c.Peers), + "the bridged policy's source peers must not be connected") + }) + + t.Run("disabled route does not bridge its ACG policies", func(t *testing.T) { + target := newPeer(targetID, 1) + remote := newPeer("peer-remote", 2) + nmd := newNMD(target, remote) + addGroup(nmd, "g-acl") + addGroup(nmd, "g-remote", remote.ID) + nmd.Routes = []*nmdata.Route{{ID: "r-1", Peer: targetID, Enabled: false, AccessControlGroups: []string{"g-acl"}}} + nmd.Policies = []*nmdata.Policy{newPolicy("p-acl", newRule([]string{"g-remote"}, []string{"g-acl"}))} + + c := compute(nmd, targetID) + + assert.Empty(t, c.Policies) + }) +} + +func TestGetPeerNetworkMapComponents_SSHRequirements(t *testing.T) { + allowedUsers := map[string]struct{}{"user-1": {}, "user-2": {}} + groupUsers := map[string][]string{"g-auth": {"user-a"}, "g-other": {"user-b"}} + + cases := []struct { + name string + mutateRule func(*nmdata.PolicyRule) + sshEnabled bool + targetInSrc bool + wantAllowed bool + wantGroupsMap map[string][]string + }{ + { + name: "netbird-ssh with authorized groups", + mutateRule: func(r *nmdata.PolicyRule) { + r.Protocol = string(nbtypes.PolicyRuleProtocolNetbirdSSH) + r.AuthorizedGroups = map[string][]string{"g-auth": nil} + }, + wantGroupsMap: map[string][]string{"g-auth": {"user-a"}}, + }, + { + name: "netbird-ssh with authorized user", + mutateRule: func(r *nmdata.PolicyRule) { + r.Protocol = string(nbtypes.PolicyRuleProtocolNetbirdSSH) + r.AuthorizedUser = "root" + }, + }, + { + name: "netbird-ssh default needs allowed users", + mutateRule: func(r *nmdata.PolicyRule) { + r.Protocol = string(nbtypes.PolicyRuleProtocolNetbirdSSH) + }, + wantAllowed: true, + }, + { + name: "legacy all-protocol with SSH enabled", + mutateRule: func(r *nmdata.PolicyRule) { r.Protocol = string(nbtypes.PolicyRuleProtocolALL) }, + sshEnabled: true, + wantAllowed: true, + }, + { + name: "legacy all-protocol with SSH disabled", + mutateRule: func(r *nmdata.PolicyRule) { r.Protocol = string(nbtypes.PolicyRuleProtocolALL) }, + }, + { + name: "tcp port 22 with SSH enabled", + mutateRule: func(r *nmdata.PolicyRule) { r.Ports = []string{"22"} }, + sshEnabled: true, + wantAllowed: true, + }, + { + name: "tcp port range covering 22", + mutateRule: func(r *nmdata.PolicyRule) { r.PortRanges = []nmdata.RulePortRange{{Start: 20, End: 30}} }, + sshEnabled: true, + wantAllowed: true, + }, + { + name: "tcp native ssh port 22022", + mutateRule: func(r *nmdata.PolicyRule) { r.Ports = []string{"22022"} }, + sshEnabled: true, + wantAllowed: true, + }, + { + name: "tcp port range covering only native ssh port", + mutateRule: func(r *nmdata.PolicyRule) { r.PortRanges = []nmdata.RulePortRange{{Start: 22000, End: 23000}} }, + sshEnabled: true, + wantAllowed: true, + }, + { + name: "tcp unrelated port", + mutateRule: func(r *nmdata.PolicyRule) { r.Ports = []string{"443"} }, + sshEnabled: true, + }, + { + name: "netbird-ssh only counts on the destination side", + mutateRule: func(r *nmdata.PolicyRule) { + r.Protocol = string(nbtypes.PolicyRuleProtocolNetbirdSSH) + }, + targetInSrc: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + target := newPeer(targetID, 1) + target.SSHEnabled = tc.sshEnabled + admin := newPeer("peer-admin", 2) + nmd := newNMD(target, admin) + nmd.AllowedUserIDs = allowedUsers + nmd.GroupIDToUserIDs = groupUsers + addGroup(nmd, "g-adm", admin.ID) + addGroup(nmd, "g-tgt", targetID) + rule := newRule([]string{"g-adm"}, []string{"g-tgt"}) + if tc.targetInSrc { + rule = newRule([]string{"g-tgt"}, []string{"g-adm"}) + } + tc.mutateRule(rule) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", rule)} + + c := compute(nmd, targetID) + + if tc.wantAllowed { + assert.Equal(t, allowedUsers, c.AllowedUserIDs) + } else { + assert.Nil(t, c.AllowedUserIDs) + } + assert.Equal(t, tc.wantGroupsMap, c.GroupIDToUserIDs) + }) + } +} + +func TestGetPeerNetworkMapComponents_DNSRecordFiltering(t *testing.T) { + record := func(name, rdata string) nmdata.SimpleRecord { + return nmdata.SimpleRecord{Name: name, Type: 1, Class: "IN", TTL: 300, RData: rdata} + } + + build := func(ipv6Target bool) (*networkmap.NetworkMapData, nmdata.CustomZone) { + target := newPeer(targetID, 1) + if ipv6Target { + target.IPv6 = netip.MustParseAddr("fd00::1") + target.Meta.Capabilities = []int32{nmdata.PeerCapabilityIPv6Overlay} + } + buddy := newPeer("peer-buddy", 2) + buddy.IPv6 = netip.MustParseAddr("fd00::2") + stranger := newPeer("peer-stranger", 3) + nmd := newNMD(target, buddy, stranger) + addGroup(nmd, "g-src", targetID) + addGroup(nmd, "g-dst", buddy.ID) + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", newRule([]string{"g-src"}, []string{"g-dst"}))} + zone := nmdata.CustomZone{ + Domain: "acme.netbird.cloud.", + Records: []nmdata.SimpleRecord{ + record(targetID, "100.64.0.1"), + record("peer-buddy", "100.64.0.2"), + record("peer-stranger", "100.64.0.3"), + record("outsider", "9.9.9.9"), + record("peer-buddy-v6", "fd00::2"), + }, + } + return nmd, zone + } + + t.Run("records limited to relevant peers, IPv6 dropped without capability", func(t *testing.T) { + nmd, zone := build(false) + + c := nmd.GetPeerNetworkMapComponents(targetID, zone) + + assert.Equal(t, "acme.netbird.cloud.", c.CustomZoneDomain) + assert.Equal(t, []nmdata.SimpleRecord{ + record(targetID, "100.64.0.1"), + record("peer-buddy", "100.64.0.2"), + }, c.AllDNSRecords) + }) + + t.Run("IPv6 records of relevant peers kept for capable target", func(t *testing.T) { + nmd, zone := build(true) + + c := nmd.GetPeerNetworkMapComponents(targetID, zone) + + assert.Equal(t, []nmdata.SimpleRecord{ + record(targetID, "100.64.0.1"), + record("peer-buddy", "100.64.0.2"), + record("peer-buddy-v6", "fd00::2"), + }, c.AllDNSRecords) + }) + + t.Run("no records yields nil", func(t *testing.T) { + nmd, _ := build(false) + + c := nmd.GetPeerNetworkMapComponents(targetID, nmdata.CustomZone{Domain: "acme.netbird.cloud."}) + + assert.Nil(t, c.AllDNSRecords) + }) +} + +func TestGetPeerNetworkMapComponents_AccountZones(t *testing.T) { + rec := func(name string) nmdata.SimpleRecord { + return nmdata.SimpleRecord{Name: name, Type: 1, Class: "IN", RData: "100.64.0.9"} + } + + t.Run("applied and private service zones for peer groups", func(t *testing.T) { + target := newPeer(targetID, 1) + nmd := newNMD(target) + addGroup(nmd, "g-a", targetID) + appliedZone := nmdata.CustomZone{Domain: "zone-one.example.com.", Records: []nmdata.SimpleRecord{rec("z1")}} + nmd.AppliedZoneCandidates = []networkmap.AppliedZoneCandidate{ + {DistributionGroups: []string{"g-a"}, Zone: appliedZone}, + {DistributionGroups: []string{"g-x"}, Zone: nmdata.CustomZone{Domain: "zone-two.example.com."}}, + } + nmd.PrivateServiceCandidates = []networkmap.PrivateServiceCandidate{ + {AccessGroups: []string{"g-a"}, Zone: nmdata.CustomZone{Domain: "svc.example.com", SearchDomainDisabled: true, NonAuthoritative: true, Records: []nmdata.SimpleRecord{rec("svc-1")}}}, + {AccessGroups: []string{"g-a"}, Zone: nmdata.CustomZone{Domain: "svc.example.com", Records: []nmdata.SimpleRecord{rec("svc-2")}}}, + {AccessGroups: []string{"g-x"}, Zone: nmdata.CustomZone{Domain: "other.example.com", Records: []nmdata.SimpleRecord{rec("other")}}}, + {AccessGroups: []string{"g-a"}, Zone: nmdata.CustomZone{Domain: "empty.example.com"}}, + } + + c := compute(nmd, targetID) + + require.Len(t, c.AccountZones, 2) + assert.Equal(t, appliedZone, c.AccountZones[0]) + assert.Equal(t, nmdata.CustomZone{ + Domain: "svc.example.com", + SearchDomainDisabled: true, + NonAuthoritative: true, + Records: []nmdata.SimpleRecord{rec("svc-1"), rec("svc-2")}, + }, c.AccountZones[1], "same-apex private service candidates must merge, flags from the first") + }) + + t.Run("groupless peer receives no zones", func(t *testing.T) { + target := newPeer(targetID, 1) + nmd := newNMD(target) + nmd.AppliedZoneCandidates = []networkmap.AppliedZoneCandidate{ + {DistributionGroups: []string{"g-a"}, Zone: nmdata.CustomZone{Domain: "zone-one.example.com."}}, + } + nmd.PrivateServiceCandidates = []networkmap.PrivateServiceCandidate{ + {AccessGroups: []string{"g-a"}, Zone: nmdata.CustomZone{Domain: "svc.example.com", Records: []nmdata.SimpleRecord{rec("svc")}}}, + } + + c := compute(nmd, targetID) + + assert.Empty(t, c.AccountZones) + }) +} + +func TestGetPeerNetworkMapComponents_NameServerGroups(t *testing.T) { + target := newPeer(targetID, 1) + other := newPeer("peer-other", 2) + nmd := newNMD(target, other) + addGroup(nmd, "g-own", targetID) + addGroup(nmd, "g-dst", other.ID) + addGroup(nmd, "g-foreign") + nmd.Policies = []*nmdata.Policy{newPolicy("p-1", newRule([]string{"g-own"}, []string{"g-dst"}))} + nsOwn := &nmdata.NameServerGroup{ID: "ns-own", Enabled: true, Groups: []string{"g-own"}} + nsDst := &nmdata.NameServerGroup{ID: "ns-dst", Enabled: true, Groups: []string{"g-dst"}} + nsDisabled := &nmdata.NameServerGroup{ID: "ns-disabled", Enabled: false, Groups: []string{"g-own"}} + nsForeign := &nmdata.NameServerGroup{ID: "ns-foreign", Enabled: true, Groups: []string{"g-foreign"}} + nsBoth := &nmdata.NameServerGroup{ID: "ns-both", Enabled: true, Groups: []string{"g-own", "g-dst"}} + nmd.NameServerGroups = []*nmdata.NameServerGroup{nsOwn, nil, nsDst, nsDisabled, nsForeign, nsBoth} + + c := compute(nmd, targetID) + + assert.Equal(t, []*nmdata.NameServerGroup{nsOwn, nsDst, nsBoth}, c.NameServerGroups, + "nameserver groups attach to any relevant group and ship once even when several groups match") +} + +func TestGetPeerNetworkMapComponents_NetworkResources_SourceSide(t *testing.T) { + target := newPeer(targetID, 1) + routerOK := newPeer("peer-router-ok", 2) + routerUnval := newPeer("peer-router-unval", 3) + nmd := newNMD(target, routerOK, routerUnval) + delete(nmd.ValidatedPeers, routerUnval.ID) + addGroup(nmd, "g-clients", targetID) + addGroup(nmd, "g-resource") + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + rp := newPolicy("rp-1", newRule([]string{"g-clients"}, []string{"g-resource"})) + nmd.Policies = []*nmdata.Policy{rp} + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {rp}} + routers := map[string]*nmdata.NetworkRouter{ + routerOK.ID: {Metric: 100}, + routerUnval.ID: {Metric: 200}, + } + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": routers} + + c := compute(nmd, targetID) + + assert.Equal(t, []*nmdata.NetworkResource{res}, c.NetworkResources) + assert.Equal(t, map[string][]*nmdata.Policy{"res-1": {rp}}, c.ResourcePoliciesMap) + assert.Equal(t, map[string]map[string]*nmdata.NetworkRouter{"net-1": routers}, c.RoutersMap) + assert.ElementsMatch(t, []string{routerOK.ID, routerUnval.ID}, peerIDSet(c.RouterPeers), + "RouterPeers carries all routing peers regardless of validation") + assert.ElementsMatch(t, []string{targetID, routerOK.ID}, peerIDSet(c.Peers), + "only validated routing peers are connected") + assert.ElementsMatch(t, []string{"g-clients", "g-resource"}, groupIDSet(c.Groups)) +} + +func TestGetPeerNetworkMapComponents_NetworkResources_RouterSide(t *testing.T) { + t.Run("posture-valid validated source peers connected, failures recorded", func(t *testing.T) { + target := newPeer(targetID, 1) + clientOK := newPeer("peer-client-ok", 2) + clientUnval := newPeer("peer-client-unval", 3) + clientFail := newPeer("peer-client-fail", 4) + clientFail.Meta.WtVersion = failingVersion + nmd := newNMD(target, clientOK, clientUnval, clientFail) + delete(nmd.ValidatedPeers, clientUnval.ID) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-clients", clientOK.ID, clientUnval.ID, clientFail.ID) + addGroup(nmd, "g-resource") + addGroup(nmd, "g-tgt", targetID) + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + rp := newPolicy("rp-1", newRule([]string{"g-clients"}, []string{"g-resource"})) + rp.SourcePostureChecks = []string{"pc-1"} + acl := newPolicy("p-acl", newRule([]string{"g-clients"}, []string{"g-tgt"})) + nmd.Policies = []*nmdata.Policy{acl} + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {rp}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {targetID: {Metric: 100}}} + + c := compute(nmd, targetID) + + assert.Equal(t, []*nmdata.NetworkResource{res}, c.NetworkResources) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.RouterPeers)) + assert.ElementsMatch(t, []string{targetID, clientOK.ID, clientFail.ID}, peerIDSet(c.Peers), + "clientFail connects via the open ACL policy, clientUnval never connects") + assert.Equal(t, map[string]map[string]struct{}{"pc-1": {clientFail.ID: {}}}, c.PostureFailedPeers) + }) + + t.Run("peer source resource collects exactly that peer", func(t *testing.T) { + target := newPeer(targetID, 1) + client := newPeer("peer-client", 2) + other := newPeer("peer-other", 3) + nmd := newNMD(target, client, other) + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + rule := newRule(nil, nil) + rule.SourceResource = nmdata.Resource{ID: client.ID, Type: string(nbtypes.ResourceTypePeer)} + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {newPolicy("rp-1", rule)}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {targetID: {}}} + + c := compute(nmd, targetID) + + assert.Equal(t, []*nmdata.NetworkResource{res}, c.NetworkResources) + assert.ElementsMatch(t, []string{targetID, client.ID}, peerIDSet(c.Peers)) + }) + + t.Run("multiple source groups unioned, missing group tolerated", func(t *testing.T) { + target := newPeer(targetID, 1) + c1 := newPeer("peer-c1", 2) + c2 := newPeer("peer-c2", 3) + nmd := newNMD(target, c1, c2) + addGroup(nmd, "g-c1", c1.ID) + addGroup(nmd, "g-c2", c2.ID) + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + nmd.ResourcePolicies = map[string][]*nmdata.Policy{ + "res-1": {newPolicy("rp-1", newRule([]string{"g-c1", "g-c2", "g-ghost"}, nil))}, + } + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {targetID: {}}} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, c1.ID, c2.ID}, peerIDSet(c.Peers)) + }) +} + +func TestGetPeerNetworkMapComponents_NetworkResources_PeerResourceSource(t *testing.T) { + build := func(sourcePeerID string) *networkmap.NetworkMapData { + target := newPeer(targetID, 1) + router := newPeer("peer-router", 2) + other := newPeer("peer-other", 3) + nmd := newNMD(target, router, other) + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + rule := newRule(nil, nil) + rule.SourceResource = nmdata.Resource{ID: sourcePeerID, Type: string(nbtypes.ResourceTypePeer)} + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {newPolicy("rp-1", rule)}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {"peer-router": {}}} + return nmd + } + + t.Run("target named as source resource gains access", func(t *testing.T) { + c := compute(build(targetID), targetID) + + assert.Len(t, c.NetworkResources, 1) + assert.ElementsMatch(t, []string{targetID, "peer-router"}, peerIDSet(c.Peers)) + }) + + t.Run("other peer named as source resource denies target", func(t *testing.T) { + c := compute(build("peer-other"), targetID) + + assert.Empty(t, c.NetworkResources) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) +} + +func TestGetPeerNetworkMapComponents_NetworkResources_Gating(t *testing.T) { + build := func() (*networkmap.NetworkMapData, *nmdata.NetworkResource, *nmdata.Policy) { + target := newPeer(targetID, 1) + router := newPeer("peer-router", 2) + nmd := newNMD(target, router) + addGroup(nmd, "g-clients", targetID) + addGroup(nmd, "g-resource") + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + rp := newPolicy("rp-1", newRule([]string{"g-clients"}, []string{"g-resource"})) + nmd.Policies = []*nmdata.Policy{rp} + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {rp}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {router.ID: {}}} + return nmd, res, rp + } + + assertResourceSkipped := func(t *testing.T, c *nbtypes.NetworkMapComponents) { + t.Helper() + assert.Empty(t, c.NetworkResources) + assert.Empty(t, c.RoutersMap) + assert.Empty(t, c.RouterPeers) + assert.Empty(t, c.ResourcePoliciesMap) + } + + t.Run("baseline grants access", func(t *testing.T) { + nmd, res, _ := build() + c := compute(nmd, targetID) + assert.Equal(t, []*nmdata.NetworkResource{res}, c.NetworkResources) + }) + + t.Run("disabled resource skipped", func(t *testing.T) { + nmd, res, _ := build() + res.Enabled = false + assertResourceSkipped(t, compute(nmd, targetID)) + }) + + t.Run("resource without policies skipped", func(t *testing.T) { + nmd, _, _ := build() + nmd.ResourcePolicies = nil + assertResourceSkipped(t, compute(nmd, targetID)) + }) + + t.Run("peer neither router nor in sources skipped", func(t *testing.T) { + nmd, _, _ := build() + nmd.Groups["g-clients"].Peers = []string{"peer-router"} + c := compute(nmd, targetID) + assertResourceSkipped(t, c) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) + + t.Run("nil and rule-less resource policy entries tolerated", func(t *testing.T) { + nmd, res, rp := build() + nmd.NetworkResources = append([]*nmdata.NetworkResource{nil}, nmd.NetworkResources...) + rp.Rules = append(rp.Rules, nil) + nmd.ResourcePolicies["res-1"] = append([]*nmdata.Policy{nil, {ID: "rp-empty", Enabled: true}}, nmd.ResourcePolicies["res-1"]...) + + c := compute(nmd, targetID) + + assert.Equal(t, []*nmdata.NetworkResource{res}, c.NetworkResources, + "poisoned sibling entries must not prevent the valid policy from granting access") + assert.NotPanics(t, func() { c.Calculate(context.Background()) }, + "the downstream network map calculation must survive the poisoned components") + }) + + t.Run("granting policy without routers still ships the resource", func(t *testing.T) { + nmd, res, rp := build() + nmd.Routers = nil + c := compute(nmd, targetID) + assert.Equal(t, []*nmdata.NetworkResource{res}, c.NetworkResources) + assert.Equal(t, map[string][]*nmdata.Policy{"res-1": {rp}}, c.ResourcePoliciesMap) + assert.Contains(t, c.RoutersMap, "net-1") + assert.Empty(t, c.RoutersMap["net-1"]) + assert.Empty(t, c.RouterPeers) + }) + + t.Run("target failing resource policy posture check skipped", func(t *testing.T) { + nmd, _, rp := build() + addVersionCheck(nmd, "pc-1", postureMinVersion) + rp.SourcePostureChecks = []string{"pc-1"} + nmd.Policies = nil + nmd.Peers[targetID].Meta.WtVersion = failingVersion + c := compute(nmd, targetID) + assertResourceSkipped(t, c) + assert.Empty(t, c.PostureFailedPeers) + assert.ElementsMatch(t, []string{targetID}, peerIDSet(c.Peers)) + }) +} + +// Legacy parity: resource-policy access consults only Rules[0] for peer-type +// sources, while group sources union across all rules via SourceGroups. +func TestGetPeerNetworkMapComponents_MultiRulePolicies(t *testing.T) { + t.Run("policy matching via multiple rules ships once", func(t *testing.T) { + target := newPeer(targetID, 1) + a := newPeer("peer-a", 2) + b := newPeer("peer-b", 3) + nmd := newNMD(target, a, b) + addGroup(nmd, "g-tgt", targetID) + addGroup(nmd, "g-a", a.ID) + addGroup(nmd, "g-b", b.ID) + p := newPolicy("p-1", + newRule([]string{"g-tgt"}, []string{"g-a"}), + newRule([]string{"g-tgt"}, []string{"g-b"})) + nmd.Policies = []*nmdata.Policy{p} + + c := compute(nmd, targetID) + + assert.Equal(t, []string{"p-1"}, policyIDs(c.Policies)) + assert.ElementsMatch(t, []string{targetID, a.ID, b.ID}, peerIDSet(c.Peers)) + }) + + t.Run("resource access consults only the first rule's peer source", func(t *testing.T) { + target := newPeer(targetID, 1) + other := newPeer("peer-other", 2) + router := newPeer("peer-router", 3) + nmd := newNMD(target, other, router) + addGroup(nmd, "g-other", other.ID) + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + second := newRule(nil, nil) + second.SourceResource = nmdata.Resource{ID: targetID, Type: string(nbtypes.ResourceTypePeer)} + rp := newPolicy("rp-1", newRule([]string{"g-other"}, nil), second) + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {rp}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {router.ID: {}}} + + c := compute(nmd, targetID) + + assert.Empty(t, c.NetworkResources, + "a second rule naming the target as peer source must not grant resource access") + }) + + t.Run("router-side source collection consults only the first rule's peer source", func(t *testing.T) { + target := newPeer(targetID, 1) + x := newPeer("peer-x", 2) + y := newPeer("peer-y", 3) + nmd := newNMD(target, x, y) + addGroup(nmd, "g-y", y.ID) + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + first := newRule(nil, nil) + first.SourceResource = nmdata.Resource{ID: x.ID, Type: string(nbtypes.ResourceTypePeer)} + rp := newPolicy("rp-1", first, newRule([]string{"g-y"}, nil)) + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {rp}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {targetID: {}}} + + c := compute(nmd, targetID) + + assert.ElementsMatch(t, []string{targetID, x.ID}, peerIDSet(c.Peers), + "second-rule group sources are not collected when the first rule names a peer") + }) +} + +// Characterization of legacy parity: once one resource policy grants the peer +// access, the source peers of the resource's subsequent policies are collected +// as if the peer were a router. +func TestGetPeerNetworkMapComponents_NetworkResources_LaterPoliciesContributeSourcePeers(t *testing.T) { + target := newPeer(targetID, 1) + otherSrc := newPeer("peer-other-src", 2) + router := newPeer("peer-router", 3) + nmd := newNMD(target, otherSrc, router) + addGroup(nmd, "g-a", targetID) + addGroup(nmd, "g-b", otherSrc.ID) + addGroup(nmd, "g-resource") + res := &nmdata.NetworkResource{ID: "res-1", NetworkID: "net-1", Enabled: true} + nmd.NetworkResources = []*nmdata.NetworkResource{res} + rpA := newPolicy("rp-a", newRule([]string{"g-a"}, []string{"g-resource"})) + rpB := newPolicy("rp-b", newRule([]string{"g-b"}, []string{"g-resource"})) + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {rpA, rpB}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {router.ID: {}}} + + c := compute(nmd, targetID) + + assert.Contains(t, c.Peers, otherSrc.ID) +} + +func TestGetPeerNetworkMapComponents_StoreImmutableAndDeterministic(t *testing.T) { + zone := nmdata.CustomZone{ + Domain: "acme.netbird.cloud.", + Records: []nmdata.SimpleRecord{{Name: "peer-src", Type: 1, Class: "IN", TTL: 300, RData: "100.64.0.2"}}, + } + build := func() *networkmap.NetworkMapData { + target := newPeer(targetID, 1) + src := newPeer("peer-src", 2) + failer := newPeer("peer-failer", 3) + failer.Meta.WtVersion = failingVersion + router := newPeer("peer-router", 4) + resRouter := newPeer("peer-res-router", 5) + nmd := newNMD(target, src, failer, router, resRouter) + addVersionCheck(nmd, "pc-1", postureMinVersion) + addGroup(nmd, "g-src", src.ID, failer.ID) + addGroup(nmd, "g-dst", targetID, src.ID) + addGroup(nmd, "g-dist", targetID, src.ID) + addGroup(nmd, "g-auth", src.ID) + addGroup(nmd, "g-clients", targetID) + addGroup(nmd, "g-resource") + nmd.AllowedUserIDs = map[string]struct{}{"user-1": {}} + nmd.GroupIDToUserIDs = map[string][]string{"g-auth": {"user-a"}} + checked := newPolicy("p-checked", newRule([]string{"g-src"}, []string{"g-dst"})) + checked.SourcePostureChecks = []string{"pc-1"} + open := newPolicy("p-open", newRule([]string{"g-src"}, []string{"g-dst"})) + sshAuth := newRule([]string{"g-src"}, []string{"g-dst"}) + sshAuth.Protocol = string(nbtypes.PolicyRuleProtocolNetbirdSSH) + sshAuth.AuthorizedGroups = map[string][]string{"g-auth": nil} + sshPlain := newRule([]string{"g-src"}, []string{"g-dst"}) + sshPlain.Protocol = string(nbtypes.PolicyRuleProtocolNetbirdSSH) + rp := newPolicy("rp-1", newRule([]string{"g-clients"}, []string{"g-resource"})) + nmd.Policies = []*nmdata.Policy{checked, open, newPolicy("p-ssh", sshAuth, sshPlain), rp} + nmd.Routes = []*nmdata.Route{{ID: "r-1", Peer: router.ID, Enabled: true, Groups: []string{"g-dist"}}} + nmd.NetworkResources = []*nmdata.NetworkResource{{ID: "res-1", NetworkID: "net-1", Enabled: true}} + nmd.ResourcePolicies = map[string][]*nmdata.Policy{"res-1": {rp}} + nmd.Routers = map[string]map[string]*nmdata.NetworkRouter{"net-1": {resRouter.ID: {Metric: 100}}} + nmd.NameServerGroups = []*nmdata.NameServerGroup{{ID: "ns-1", Enabled: true, Groups: []string{"g-dst"}}} + nmd.AppliedZoneCandidates = []networkmap.AppliedZoneCandidate{ + {DistributionGroups: []string{"g-dst"}, Zone: nmdata.CustomZone{Domain: "zone.example.com.", Records: []nmdata.SimpleRecord{{Name: "z", Type: 1, RData: "100.64.0.9"}}}}, + } + nmd.PrivateServiceCandidates = []networkmap.PrivateServiceCandidate{ + {AccessGroups: []string{"g-dst"}, Zone: nmdata.CustomZone{Domain: "svc.example.com", Records: []nmdata.SimpleRecord{{Name: "s", Type: 1, RData: "100.64.0.8"}}}}, + } + return nmd + } + + nmd := build() + groupSnapshots := make(map[string][]string, len(nmd.Groups)) + for id, g := range nmd.Groups { + groupSnapshots[id] = append([]string(nil), g.Peers...) + } + + first := nmd.GetPeerNetworkMapComponents(targetID, zone) + _ = nmd.GetPeerNetworkMapComponents("peer-src", zone) + second := nmd.GetPeerNetworkMapComponents(targetID, zone) + + for id, g := range nmd.Groups { + assert.Equal(t, groupSnapshots[id], g.Peers, "group %s mutated in the store", id) + } + + for name, field := range map[string]any{ + "Peers": first.Peers, + "PostureFailedPeers": first.PostureFailedPeers, + "RoutersMap": first.RoutersMap, + "RouterPeers": first.RouterPeers, + "NetworkResources": first.NetworkResources, + "NameServerGroups": first.NameServerGroups, + "AccountZones": first.AccountZones, + "AllDNSRecords": first.AllDNSRecords, + "AllowedUserIDs": first.AllowedUserIDs, + "GroupIDToUserIDs": first.GroupIDToUserIDs, + "ResourcePoliciesMap": first.ResourcePoliciesMap, + } { + require.NotEmpty(t, field, "fixture must populate %s or the determinism check is vacuous", name) + } + + assert.Equal(t, first.Peers, second.Peers) + assert.Equal(t, first.Groups, second.Groups) + assert.Equal(t, first.Policies, second.Policies) + assert.Equal(t, first.Routes, second.Routes) + assert.Equal(t, first.PostureFailedPeers, second.PostureFailedPeers) + assert.Equal(t, first.ResourcePoliciesMap, second.ResourcePoliciesMap) + assert.Equal(t, first.RoutersMap, second.RoutersMap) + assert.Equal(t, first.RouterPeers, second.RouterPeers) + assert.Equal(t, first.NetworkResources, second.NetworkResources) + assert.Equal(t, first.NameServerGroups, second.NameServerGroups) + assert.Equal(t, first.AccountZones, second.AccountZones) + assert.Equal(t, first.AllDNSRecords, second.AllDNSRecords) + assert.Equal(t, first.AllowedUserIDs, second.AllowedUserIDs) + assert.Equal(t, first.GroupIDToUserIDs, second.GroupIDToUserIDs) +} + +func TestNetworkMapData_GetPeerGroups(t *testing.T) { + target := newPeer(targetID, 1) + other := newPeer("peer-other", 2) + nmd := newNMD(target, other) + addGroup(nmd, "g-1", targetID, other.ID) + addGroup(nmd, "g-2", targetID) + addGroup(nmd, "g-3", other.ID) + nmd.Groups["g-nil"] = nil + + assert.Equal(t, map[string]struct{}{"g-1": {}, "g-2": {}}, nmd.GetPeerGroups(targetID)) + assert.Empty(t, nmd.GetPeerGroups("missing")) +}