Files
netbird/management/server/affected_peers_test.go
2026-05-07 16:55:45 +02:00

1979 lines
60 KiB
Go

package server
import (
"context"
"fmt"
"net/netip"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
nbdns "github.com/netbirdio/netbird/dns"
routerTypes "github.com/netbirdio/netbird/management/server/networks/routers/types"
networkTypes "github.com/netbirdio/netbird/management/server/networks/types"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/route"
)
// setupAffectedPeersTest creates a manager with a clean account (default policy deleted)
// and 5 peers, each in its own group: peer0->group0, peer1->group1, ..., peer4->group4.
func setupAffectedPeersTest(t *testing.T) (*DefaultAccountManager, store.Store, string, []string, []string) {
t.Helper()
manager, _, err := createManager(t)
require.NoError(t, err)
account, err := createAccount(manager, "affected_test", userID, "")
require.NoError(t, err)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
setupKey, err := manager.CreateSetupKey(ctx, accountID, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
require.NoError(t, err)
peerIDs := make([]string, 5)
for i := 0; i < 5; i++ {
peer := addPeerToAccount(t, manager, accountID, setupKey.Key)
peerIDs[i] = peer.ID
}
groupIDs := make([]string, 5)
for i := 0; i < 5; i++ {
g := &types.Group{
ID: affectedGroupID(i),
Name: affectedGroupName(i),
Peers: []string{peerIDs[i]},
}
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
groupIDs[i] = g.ID
}
return manager, manager.Store, accountID, peerIDs, groupIDs
}
func affectedGroupID(i int) string { return fmt.Sprintf("affected-grp-%d", i) }
func affectedGroupName(i int) string { return fmt.Sprintf("AffectedGroup%d", i) }
func TestCollectGroupChange_NoEntities(t *testing.T) {
_, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Empty(t, groups)
assert.Empty(t, directPeers)
}
func TestCollectGroupChange_EmptyInput(t *testing.T) {
_, s, accountID, _, _ := setupAffectedPeersTest(t)
ctx := context.Background()
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, nil)
assert.Nil(t, groups)
assert.Nil(t, directPeers)
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{})
assert.Nil(t, groups)
assert.Nil(t, directPeers)
}
func TestCollectGroupChange_PolicyLinked(t *testing.T) {
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[1]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
groups, _ := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, groups, groupIDs[1])
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[1]})
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, groups, groupIDs[1])
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[2]})
assert.Empty(t, groups)
}
func TestCollectGroupChange_PolicyWithDirectPeerResource(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
SourceResource: types.Resource{ID: peerIDs[3], Type: types.ResourceTypePeer},
Destinations: []string{groupIDs[1]},
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, groups, groupIDs[1])
assert.Contains(t, directPeers, peerIDs[3])
}
func TestCollectGroupChange_PolicyWithNonPeerResource_NoDirectPeers(t *testing.T) {
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
SourceResource: types.Resource{ID: "some-domain", Type: types.ResourceTypeDomain},
Destinations: []string{groupIDs[1]},
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, groups, groupIDs[1])
assert.Empty(t, directPeers, "non-peer resources should not produce direct peer IDs")
}
func TestCollectGroupChange_RouteLinked(t *testing.T) {
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.0.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{groupIDs[0]},
"test route",
"testnet",
false,
9999,
[]string{groupIDs[1]},
[]string{groupIDs[2]},
true,
userID,
false,
false,
)
require.NoError(t, err)
groups, _ := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, groups, groupIDs[1])
assert.Contains(t, groups, groupIDs[2])
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[1]})
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, groups, groupIDs[1])
assert.Contains(t, groups, groupIDs[2])
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[3]})
assert.Empty(t, groups)
}
func TestCollectGroupChange_RouteWithDirectPeer(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.1.0.0/24"),
route.IPv4Network,
nil,
peerIDs[4],
nil,
"test route peer",
"testnet2",
false,
9999,
[]string{groupIDs[1]},
nil,
true,
userID,
false,
false,
)
require.NoError(t, err)
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[1]})
assert.Contains(t, groups, groupIDs[1])
assert.Contains(t, directPeers, peerIDs[4])
}
func TestCollectGroupChange_NameServerGroupLinked(t *testing.T) {
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.CreateNameServerGroup(ctx, accountID, "ns1", "NS Group 1",
[]nbdns.NameServer{{
IP: netip.MustParseAddr("1.1.1.1"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
}},
[]string{groupIDs[0]},
true, nil, true, userID, false,
)
require.NoError(t, err)
groups, _ := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Contains(t, groups, groupIDs[0])
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[1]})
assert.Empty(t, groups)
}
func TestCollectGroupChange_DNSSettingsLinked(t *testing.T) {
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
err := manager.SaveDNSSettings(ctx, accountID, userID, &types.DNSSettings{
DisabledManagementGroups: []string{groupIDs[2]},
})
require.NoError(t, err)
groups, _ := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[2]})
assert.Contains(t, groups, groupIDs[2])
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Empty(t, groups)
}
func TestCollectGroupChange_NetworkRouterLinked(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
net1 := &networkTypes.Network{
ID: "net-test-1",
AccountID: accountID,
Name: "test-network",
}
err := manager.Store.SaveNetwork(ctx, net1)
require.NoError(t, err)
err = manager.Store.SaveNetworkRouter(ctx, &routerTypes.NetworkRouter{
ID: "router1",
NetworkID: net1.ID,
AccountID: accountID,
PeerGroups: []string{groupIDs[0]},
Peer: peerIDs[3],
})
require.NoError(t, err)
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, directPeers, peerIDs[3])
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[1]})
assert.Empty(t, groups)
assert.Empty(t, directPeers)
}
func TestCollectGroupChange_NetworkRouterPeerOnlyNoGroups(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
net1 := &networkTypes.Network{
ID: "net-peer-only",
AccountID: accountID,
Name: "peer-only-network",
}
err := manager.Store.SaveNetwork(ctx, net1)
require.NoError(t, err)
// Router with only a direct peer, no PeerGroups
err = manager.Store.SaveNetworkRouter(ctx, &routerTypes.NetworkRouter{
ID: "router-peer-only",
NetworkID: net1.ID,
AccountID: accountID,
Peer: peerIDs[4],
})
require.NoError(t, err)
// None of the groups should match since router has no PeerGroups
for i := 0; i < 5; i++ {
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[i]})
assert.Empty(t, groups, "group%d should not match router with only direct peer", i)
assert.Empty(t, directPeers, "group%d should not produce direct peers", i)
}
}
func TestCollectGroupChange_MultipleEntities(t *testing.T) {
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[1]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
_, err = manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.2.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{groupIDs[2]},
"multi route",
"multinet",
false,
9999,
[]string{groupIDs[3]},
nil,
true,
userID,
false,
false,
)
require.NoError(t, err)
groups, directPeers := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, groups, groupIDs[1])
assert.NotContains(t, groups, groupIDs[2])
assert.NotContains(t, groups, groupIDs[3])
assert.Empty(t, directPeers)
groups, directPeers = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[3]})
assert.Contains(t, groups, groupIDs[2])
assert.Contains(t, groups, groupIDs[3])
assert.NotContains(t, groups, groupIDs[0])
assert.NotContains(t, groups, groupIDs[1])
assert.Empty(t, directPeers)
}
func TestCollectGroupChange_MultipleNameServerGroups_OnlyLinkedAffected(t *testing.T) {
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
// Create two nameserver groups using different groups
_, err := manager.CreateNameServerGroup(ctx, accountID, "ns-a", "NS-A",
[]nbdns.NameServer{{
IP: netip.MustParseAddr("1.1.1.1"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
}},
[]string{groupIDs[0]},
true, nil, true, userID, false,
)
require.NoError(t, err)
_, err = manager.CreateNameServerGroup(ctx, accountID, "ns-b", "NS-B",
[]nbdns.NameServer{{
IP: netip.MustParseAddr("8.8.8.8"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
}},
[]string{groupIDs[2]},
true, nil, true, userID, false,
)
require.NoError(t, err)
// Changing group0 should only find group0 (from ns-a), not group2 (from ns-b)
groups, _ := collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[0]})
assert.Contains(t, groups, groupIDs[0])
assert.NotContains(t, groups, groupIDs[2])
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[2]})
assert.Contains(t, groups, groupIDs[2])
assert.NotContains(t, groups, groupIDs[0])
// Unrelated group
groups, _ = collectGroupChangeAffectedGroups(ctx, s, accountID, []string{groupIDs[4]})
assert.Empty(t, groups)
}
// ---------------------------------------------------------------------------
// collectPolicyAffectedGroupsAndPeers unit tests
// ---------------------------------------------------------------------------
func TestCollectPolicyAffectedGroups_Basic(t *testing.T) {
policy := &types.Policy{
Rules: []*types.PolicyRule{
{
Sources: []string{"g1", "g2"},
Destinations: []string{"g3"},
},
},
}
groups, directPeers := collectPolicyAffectedGroupsAndPeers(context.Background(), policy)
assert.ElementsMatch(t, []string{"g1", "g2", "g3"}, groups)
assert.Empty(t, directPeers)
}
func TestCollectPolicyAffectedGroups_WithPeerResources(t *testing.T) {
policy := &types.Policy{
Rules: []*types.PolicyRule{
{
Sources: []string{"g1"},
SourceResource: types.Resource{ID: "p1", Type: types.ResourceTypePeer},
Destinations: []string{"g2"},
DestinationResource: types.Resource{ID: "p2", Type: types.ResourceTypePeer},
},
},
}
groups, directPeers := collectPolicyAffectedGroupsAndPeers(context.Background(), policy)
assert.ElementsMatch(t, []string{"g1", "g2"}, groups)
assert.ElementsMatch(t, []string{"p1", "p2"}, directPeers)
}
func TestCollectPolicyAffectedGroups_NilPolicy(t *testing.T) {
groups, directPeers := collectPolicyAffectedGroupsAndPeers(context.Background(), nil)
assert.Nil(t, groups)
assert.Nil(t, directPeers)
}
func TestCollectPolicyAffectedGroups_MultipleRules(t *testing.T) {
policy := &types.Policy{
Rules: []*types.PolicyRule{
{Sources: []string{"g1"}, Destinations: []string{"g2"}},
{Sources: []string{"g3"}, Destinations: []string{"g4"}},
},
}
groups, _ := collectPolicyAffectedGroupsAndPeers(context.Background(), policy)
assert.ElementsMatch(t, []string{"g1", "g2", "g3", "g4"}, groups)
}
func TestCollectPolicyAffectedGroups_MultiplePolicies(t *testing.T) {
old := &types.Policy{
Rules: []*types.PolicyRule{
{Sources: []string{"g1"}, Destinations: []string{"g2"}},
},
}
new := &types.Policy{
Rules: []*types.PolicyRule{
{Sources: []string{"g3"}, Destinations: []string{"g4"}},
},
}
groups, _ := collectPolicyAffectedGroupsAndPeers(context.Background(), new, old)
assert.ElementsMatch(t, []string{"g1", "g2", "g3", "g4"}, groups)
}
func TestCollectPolicyAffectedGroups_EmptyRules(t *testing.T) {
policy := &types.Policy{Rules: []*types.PolicyRule{}}
groups, directPeers := collectPolicyAffectedGroupsAndPeers(context.Background(), policy)
assert.Empty(t, groups)
assert.Empty(t, directPeers)
}
func TestCollectPolicyAffectedGroups_NonPeerResource(t *testing.T) {
policy := &types.Policy{
Rules: []*types.PolicyRule{
{
Sources: []string{"g1"},
SourceResource: types.Resource{ID: "domain-1", Type: types.ResourceTypeDomain},
Destinations: []string{"g2"},
},
},
}
groups, directPeers := collectPolicyAffectedGroupsAndPeers(context.Background(), policy)
assert.ElementsMatch(t, []string{"g1", "g2"}, groups)
assert.Empty(t, directPeers, "domain resource type should not produce direct peer IDs")
}
// ---------------------------------------------------------------------------
// collectRouteAffectedGroupsAndPeers unit tests
// ---------------------------------------------------------------------------
func TestCollectRouteAffectedGroups_Basic(t *testing.T) {
r := &route.Route{
Groups: []string{"g1"},
PeerGroups: []string{"g2"},
AccessControlGroups: []string{"g3"},
}
groups, directPeers := collectRouteAffectedGroupsAndPeers(context.Background(), r)
assert.ElementsMatch(t, []string{"g1", "g2", "g3"}, groups)
assert.Empty(t, directPeers)
}
func TestCollectRouteAffectedGroups_WithDirectPeer(t *testing.T) {
r := &route.Route{
Groups: []string{"g1"},
Peer: "p1",
}
groups, directPeers := collectRouteAffectedGroupsAndPeers(context.Background(), r)
assert.ElementsMatch(t, []string{"g1"}, groups)
assert.ElementsMatch(t, []string{"p1"}, directPeers)
}
func TestCollectRouteAffectedGroups_NilRoute(t *testing.T) {
groups, directPeers := collectRouteAffectedGroupsAndPeers(context.Background(), nil)
assert.Nil(t, groups)
assert.Nil(t, directPeers)
}
func TestCollectRouteAffectedGroups_MultipleRoutes(t *testing.T) {
old := &route.Route{
Groups: []string{"g1"},
Peer: "p1",
}
new := &route.Route{
Groups: []string{"g2"},
PeerGroups: []string{"g3"},
}
groups, directPeers := collectRouteAffectedGroupsAndPeers(context.Background(), new, old)
assert.ElementsMatch(t, []string{"g1", "g2", "g3"}, groups)
assert.ElementsMatch(t, []string{"p1"}, directPeers)
}
// ---------------------------------------------------------------------------
// policyReferencesGroups / routeReferencesGroups / routerReferencesGroups
// ---------------------------------------------------------------------------
func TestPolicyReferencesGroups(t *testing.T) {
policy := &types.Policy{
Rules: []*types.PolicyRule{
{
Sources: []string{"g1", "g2"},
Destinations: []string{"g3"},
},
},
}
tests := []struct {
name string
groupSet map[string]struct{}
want bool
}{
{"matches source", map[string]struct{}{"g1": {}}, true},
{"matches destination", map[string]struct{}{"g3": {}}, true},
{"no match", map[string]struct{}{"g4": {}}, false},
{"empty set", map[string]struct{}{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := policyReferencesGroups(policy, tt.groupSet)
assert.Equal(t, tt.want, got)
})
}
}
func TestRouteReferencesGroups(t *testing.T) {
r := &route.Route{
Groups: []string{"g1"},
PeerGroups: []string{"g2"},
AccessControlGroups: []string{"g3"},
}
tests := []struct {
name string
groupSet map[string]struct{}
want bool
}{
{"matches groups", map[string]struct{}{"g1": {}}, true},
{"matches peerGroups", map[string]struct{}{"g2": {}}, true},
{"matches accessControl", map[string]struct{}{"g3": {}}, true},
{"no match", map[string]struct{}{"g4": {}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := routeReferencesGroups(r, tt.groupSet)
assert.Equal(t, tt.want, got)
})
}
}
func TestRouterReferencesGroups(t *testing.T) {
router := &routerTypes.NetworkRouter{
PeerGroups: []string{"g1", "g2"},
}
tests := []struct {
name string
groupSet map[string]struct{}
want bool
}{
{"matches", map[string]struct{}{"g1": {}}, true},
{"no match", map[string]struct{}{"g3": {}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := routerReferencesGroups(router, tt.groupSet)
assert.Equal(t, tt.want, got)
})
}
}
// ---------------------------------------------------------------------------
// resolvePeerIDs tests
// ---------------------------------------------------------------------------
func TestResolvePeerIDs_GroupsOnly(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
result := manager.resolvePeerIDs(ctx, s, accountID, []string{groupIDs[0], groupIDs[1]}, nil)
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
}
func TestResolvePeerIDs_WithDirectPeers(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
result := manager.resolvePeerIDs(ctx, s, accountID, []string{groupIDs[0]}, []string{peerIDs[2]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[2]}, result)
}
func TestResolvePeerIDs_Deduplication(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
result := manager.resolvePeerIDs(ctx, s, accountID, []string{groupIDs[0]}, []string{peerIDs[0]})
assert.Len(t, result, 1)
assert.Equal(t, peerIDs[0], result[0])
}
func TestResolvePeerIDs_EmptyInputs(t *testing.T) {
manager, s, accountID, _, _ := setupAffectedPeersTest(t)
ctx := context.Background()
result := manager.resolvePeerIDs(ctx, s, accountID, nil, nil)
assert.Empty(t, result)
}
// ---------------------------------------------------------------------------
// resolveAffectedPeersForPeerChanges tests
// ---------------------------------------------------------------------------
func TestResolveAffectedPeers_NoPoliciesOrRoutes(t *testing.T) {
manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t)
ctx := context.Background()
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.Empty(t, result)
}
func TestResolveAffectedPeers_PolicyBetweenTwoGroups(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[1]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[1]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
assert.Empty(t, result)
}
func TestResolveAffectedPeers_PolicyThreeGroups(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0], groupIDs[1]},
Destinations: []string{groupIDs[2]},
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2]}, result)
}
func TestResolveAffectedPeers_RoutePeerGroups(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.3.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{groupIDs[0]},
"test route",
"routenet",
false,
9999,
[]string{groupIDs[1]},
nil,
true,
userID,
false,
false,
)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[1]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
assert.Empty(t, result)
}
func TestResolveAffectedPeers_RouteWithDirectPeer(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.4.0.0/24"),
route.IPv4Network,
nil,
peerIDs[4],
nil,
"route with peer",
"routenet2",
false,
9999,
[]string{groupIDs[1]},
nil,
true,
userID,
false,
false,
)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[1]})
assert.ElementsMatch(t, []string{peerIDs[1], peerIDs[4]}, result)
}
func TestResolveAffectedPeers_RouteWithAccessControlGroups(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.7.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{groupIDs[0]},
"acl route",
"aclnet",
false,
9999,
[]string{groupIDs[1]},
[]string{groupIDs[2]},
true,
userID,
false,
false,
)
require.NoError(t, err)
// peer2 is only in AccessControlGroups, still should be affected
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2]}, result)
// peer3 is unrelated
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[3]})
assert.Empty(t, result)
}
func TestResolveAffectedPeers_NetworkRouter(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
net1 := &networkTypes.Network{
ID: "net-test-2",
AccountID: accountID,
Name: "test-net",
}
err := manager.Store.SaveNetwork(ctx, net1)
require.NoError(t, err)
err = manager.Store.SaveNetworkRouter(ctx, &routerTypes.NetworkRouter{
ID: "router-test",
NetworkID: net1.ID,
AccountID: accountID,
PeerGroups: []string{groupIDs[0]},
Peer: peerIDs[3],
})
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[3]}, result)
}
func TestResolveAffectedPeers_NameServerGroup(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.CreateNameServerGroup(ctx, accountID, "ns-test", "NS Test",
[]nbdns.NameServer{{
IP: netip.MustParseAddr("8.8.8.8"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
}},
[]string{groupIDs[0]},
true, nil, true, userID, false,
)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.Contains(t, result, peerIDs[0])
}
func TestResolveAffectedPeers_DNSSettings(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
err := manager.SaveDNSSettings(ctx, accountID, userID, &types.DNSSettings{
DisabledManagementGroups: []string{groupIDs[0]},
})
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.Contains(t, result, peerIDs[0])
}
func TestResolveAffectedPeers_PeerInMultipleGroups(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
err := manager.GroupAddPeer(ctx, accountID, groupIDs[1], peerIDs[0])
require.NoError(t, err)
_, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[2]},
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
_, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[1]},
Destinations: []string{groupIDs[3]},
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
// peer0 is in group0 AND group1, so both policies apply
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2], peerIDs[3]}, result)
}
func TestResolveAffectedPeers_MultipleChangedPeers(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[1]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
_, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[2]},
Destinations: []string{groupIDs[3]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0], peerIDs[2]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2], peerIDs[3]}, result)
}
func TestResolveAffectedPeers_SharedGroupAcrossPolicyAndRoute(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[1]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
_, err = manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.5.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{groupIDs[2]},
"shared group route",
"sharednet",
false,
9999,
[]string{groupIDs[0]},
nil,
true,
userID,
false,
false,
)
require.NoError(t, err)
// group0 is shared: policy gives peer0+peer1, route gives peer0+peer2
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1], peerIDs[2]}, result)
}
func TestResolveAffectedPeers_EmptyChangedPeers(t *testing.T) {
manager, s, accountID, _, _ := setupAffectedPeersTest(t)
ctx := context.Background()
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, nil)
assert.Empty(t, result)
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{})
assert.Empty(t, result)
}
func TestResolveAffectedPeers_NoDuplicates(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
err := manager.GroupAddPeer(ctx, accountID, groupIDs[1], peerIDs[0])
require.NoError(t, err)
err = manager.GroupAddPeer(ctx, accountID, groupIDs[2], peerIDs[0])
require.NoError(t, err)
_, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0], groupIDs[1]},
Destinations: []string{groupIDs[2]},
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
count := 0
for _, id := range result {
if id == peerIDs[0] {
count++
}
}
assert.Equal(t, 1, count, "peer0 should appear exactly once")
}
// ---------------------------------------------------------------------------
// Posture check affected peers tests
// ---------------------------------------------------------------------------
func TestCollectPostureCheckAffected_NoMatch(t *testing.T) {
_, s, accountID, _, _ := setupAffectedPeersTest(t)
ctx := context.Background()
groups, directPeers := collectPostureCheckAffectedGroupsAndPeers(ctx, s, accountID, "nonexistent-check")
assert.Empty(t, groups)
assert.Empty(t, directPeers)
}
func TestCollectPostureCheckAffected_LinkedToPolicy(t *testing.T) {
manager, s, accountID, _, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
// Create the posture check in the store so the policy validation keeps the reference.
err := s.SavePostureChecks(ctx, &posture.Checks{
ID: "pc-1",
Name: "test-posture-check",
AccountID: accountID,
})
require.NoError(t, err)
policy, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
SourcePostureChecks: []string{"pc-1"},
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[1]},
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
_ = policy
groups, directPeers := collectPostureCheckAffectedGroupsAndPeers(ctx, s, accountID, "pc-1")
assert.Contains(t, groups, groupIDs[0])
assert.Contains(t, groups, groupIDs[1])
assert.Empty(t, directPeers)
// Different posture check ID should not match
groups, directPeers = collectPostureCheckAffectedGroupsAndPeers(ctx, s, accountID, "pc-other")
assert.Empty(t, groups)
assert.Empty(t, directPeers)
}
// ---------------------------------------------------------------------------
// Isolation tests: verify peers NOT in any relevant entity are NOT affected
// ---------------------------------------------------------------------------
func TestAffectedPeers_IsolatedPolicies(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[1]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
_, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[2]},
Destinations: []string{groupIDs[3]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
assert.NotContains(t, result, peerIDs[2])
assert.NotContains(t, result, peerIDs[3])
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
assert.ElementsMatch(t, []string{peerIDs[2], peerIDs[3]}, result)
assert.NotContains(t, result, peerIDs[0])
assert.NotContains(t, result, peerIDs[1])
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[4]})
assert.Empty(t, result)
}
func TestAffectedPeers_IsolatedRouteAndPolicy(t *testing.T) {
manager, s, accountID, peerIDs, groupIDs := setupAffectedPeersTest(t)
ctx := context.Background()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{groupIDs[0]},
Destinations: []string{groupIDs[1]},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
_, err = manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.6.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{groupIDs[2]},
"isolated route",
"isonet",
false,
9999,
[]string{groupIDs[3]},
nil,
true,
userID,
false,
false,
)
require.NoError(t, err)
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.ElementsMatch(t, []string{peerIDs[0], peerIDs[1]}, result)
assert.NotContains(t, result, peerIDs[2])
assert.NotContains(t, result, peerIDs[3])
result = manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[2]})
assert.ElementsMatch(t, []string{peerIDs[2], peerIDs[3]}, result)
assert.NotContains(t, result, peerIDs[0])
assert.NotContains(t, result, peerIDs[1])
}
// ---------------------------------------------------------------------------
// Integration tests with update channels (peerShouldReceiveUpdate / peerShouldNotReceiveUpdate)
// ---------------------------------------------------------------------------
func TestAffectedPeers_GroupUpdateOnlyAffectsLinkedPeers(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
for _, g := range []*types.Group{
{ID: "ap-grpA", Name: "AP-A", Peers: []string{peer1.ID}},
{ID: "ap-grpB", Name: "AP-B", Peers: []string{peer2.ID}},
{ID: "ap-grpC", Name: "AP-C", Peers: []string{peer3.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
_, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{"ap-grpA"},
Destinations: []string{"ap-grpB"},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
result := manager.resolveAffectedPeersForPeerChanges(ctx, manager.Store, accountID, []string{peer1.ID})
assert.ElementsMatch(t, []string{peer1.ID, peer2.ID}, result)
// Adding peer3 to grpA makes it part of the policy, so all 3 peers get updated
t.Run("group change updates all peers in policy groups", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldReceiveUpdate(t, updMsg2)
peerShouldReceiveUpdate(t, updMsg3)
close(done)
}()
err := manager.UpdateGroup(ctx, accountID, userID, &types.Group{
ID: "ap-grpA",
Name: "AP-A",
Peers: []string{peer1.ID, peer3.ID},
})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
_ = updMsg1
_ = updMsg2
_ = updMsg3
}
func TestAffectedPeers_UnlinkedGroupChange_NoUpdates(t *testing.T) {
manager, s, accountID, peerIDs, _ := setupAffectedPeersTest(t)
ctx := context.Background()
result := manager.resolveAffectedPeersForPeerChanges(ctx, s, accountID, []string{peerIDs[0]})
assert.Empty(t, result)
}
// TestAffectedPeers_PolicyChange_UnrelatedPeerNoUpdate verifies that creating/deleting a
// policy only sends updates to peers in the policy's groups, not to unrelated peers.
func TestAffectedPeers_PolicyChange_UnrelatedPeerNoUpdate(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
for _, g := range []*types.Group{
{ID: "pol-grpA", Name: "Pol-A", Peers: []string{peer1.ID}},
{ID: "pol-grpB", Name: "Pol-B", Peers: []string{peer2.ID}},
{ID: "pol-grpC", Name: "Pol-C", Peers: []string{peer3.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
// Create policy linking only peer1 (grpA) <-> peer2 (grpB). Peer3 should not receive update.
t.Run("create policy only affects linked peers", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
_, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{"pol-grpA"},
Destinations: []string{"pol-grpB"},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_RouteChange_UnrelatedPeerNoUpdate verifies that creating a route
// only sends updates to peers in the route's groups, not to unrelated peers.
func TestAffectedPeers_RouteChange_UnrelatedPeerNoUpdate(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
for _, g := range []*types.Group{
{ID: "rt-grpA", Name: "Rt-A", Peers: []string{peer1.ID}},
{ID: "rt-grpB", Name: "Rt-B", Peers: []string{peer2.ID}},
{ID: "rt-grpC", Name: "Rt-C", Peers: []string{peer3.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
// Create route with peer groups grpA and distribution group grpB. Peer3 should not get update.
t.Run("create route only affects linked peers", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
_, err := manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.10.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{"rt-grpA"},
"test route",
"routenoaffect",
false,
9999,
[]string{"rt-grpB"},
nil,
true,
userID,
false,
false,
)
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_NameServerChange_UnrelatedPeerNoUpdate verifies that creating a
// nameserver group only sends updates to peers in its groups, not to unrelated peers.
func TestAffectedPeers_NameServerChange_UnrelatedPeerNoUpdate(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
for _, g := range []*types.Group{
{ID: "ns-grpA", Name: "NS-A", Peers: []string{peer1.ID}},
{ID: "ns-grpB", Name: "NS-B", Peers: []string{peer2.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
// Create NS group using only grpA. peer2 and peer3 should not get update.
t.Run("create nameserver group only affects linked peers", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldNotReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
_, err := manager.CreateNameServerGroup(ctx, accountID, "ns-unrelated", "NS Unrelated",
[]nbdns.NameServer{{
IP: netip.MustParseAddr("1.1.1.1"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
}},
[]string{"ns-grpA"},
true, nil, true, userID, false,
)
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_DNSSettingsChange_UnrelatedPeerNoUpdate verifies that changing DNS
// settings only sends updates to peers in the affected groups, not to unrelated peers.
func TestAffectedPeers_DNSSettingsChange_UnrelatedPeerNoUpdate(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
for _, g := range []*types.Group{
{ID: "dns-grpA", Name: "DNS-A", Peers: []string{peer1.ID}},
{ID: "dns-grpB", Name: "DNS-B", Peers: []string{peer2.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
// Save DNS settings that only affects grpA. peer2 and peer3 should not be affected.
t.Run("dns settings change only affects linked peers", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldNotReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
err := manager.SaveDNSSettings(ctx, accountID, userID, &types.DNSSettings{
DisabledManagementGroups: []string{"dns-grpA"},
})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_UnlinkedGroupChange_NoUpdateIntegration tests the full integration:
// updating a group that is NOT referenced by any policy/route/ns/dns should not send
// updates to any peer.
func TestAffectedPeers_UnlinkedGroupChange_NoUpdateIntegration(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
err = manager.CreateGroup(ctx, accountID, userID, &types.Group{
ID: "unlinked-grp",
Name: "Unlinked",
Peers: []string{peer1.ID},
})
require.NoError(t, err)
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
t.Run("updating unlinked group sends no peer updates", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldNotReceiveUpdate(t, updMsg1)
peerShouldNotReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
err := manager.UpdateGroup(ctx, accountID, userID, &types.Group{
ID: "unlinked-grp",
Name: "Unlinked",
Peers: []string{peer1.ID, peer2.ID},
})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_NetworkRouter_UnrelatedPeerNoUpdate verifies that when a network
// router is added with specific peer groups, only peers in those groups (and policy
// sources for resources) get updates. Unrelated peers should not.
func TestAffectedPeers_NetworkRouter_UnrelatedPeerNoUpdate(t *testing.T) {
// Use custom setup: delete default policy BEFORE adding peers so that
// AddPeer's BufferUpdateAffectedPeers finds no affected peers and
// doesn't schedule async updates that race with the test.
manager, updateManager, err := createManager(t)
require.NoError(t, err)
ctx := context.Background()
account, err := createAccount(manager, "nr_test_account", userID, "")
require.NoError(t, err)
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
setupKey, err := manager.CreateSetupKey(ctx, accountID, "test-key", types.SetupKeyReusable, time.Hour, nil, 999, userID, false, false)
require.NoError(t, err)
peer1 := addPeerToAccount(t, manager, accountID, setupKey.Key)
peer2 := addPeerToAccount(t, manager, accountID, setupKey.Key)
peer3 := addPeerToAccount(t, manager, accountID, setupKey.Key)
for _, g := range []*types.Group{
{ID: "nr-grpA", Name: "NR-A", Peers: []string{peer1.ID}},
{ID: "nr-grpB", Name: "NR-B", Peers: []string{peer2.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
net1 := &networkTypes.Network{
ID: "nr-net-test",
AccountID: accountID,
Name: "nr-test-network",
}
err = manager.Store.SaveNetwork(ctx, net1)
require.NoError(t, err)
err = manager.Store.SaveNetworkRouter(ctx, &routerTypes.NetworkRouter{
ID: "nr-router-test",
NetworkID: net1.ID,
AccountID: accountID,
PeerGroups: []string{"nr-grpA"},
})
require.NoError(t, err)
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
// When the group linked to the network router changes, only peers in that
// group should be updated. Peer2 is unrelated. Peer3 is added to the
// router's group so it should also receive an update.
t.Run("network router group change only affects linked peers", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldNotReceiveUpdate(t, updMsg2)
peerShouldReceiveUpdate(t, updMsg3)
close(done)
}()
// Updating the group linked to router should affect peer1 and peer3 (now in nr-grpA).
err = manager.UpdateGroup(ctx, accountID, userID, &types.Group{
ID: "nr-grpA",
Name: "NR-A",
Peers: []string{peer1.ID, peer3.ID},
})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_MultipleIsolatedEntities_OnlyLinkedPeersUpdated creates multiple
// isolated entities (policy for peer1<->peer2, route for peer3) and verifies that
// changing one entity's groups only affects its peers.
func TestAffectedPeers_MultipleIsolatedEntities_OnlyLinkedPeersUpdated(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
for _, g := range []*types.Group{
{ID: "iso-grpA", Name: "ISO-A", Peers: []string{peer1.ID}},
{ID: "iso-grpB", Name: "ISO-B", Peers: []string{peer2.ID}},
{ID: "iso-grpC", Name: "ISO-C", Peers: []string{peer3.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
// Policy: peer1 <-> peer2
_, err = manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{"iso-grpA"},
Destinations: []string{"iso-grpB"},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
// Route: only peer3's group as distribution group
_, err = manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.20.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{"iso-grpC"},
"isolated route",
"isonet2",
false,
9999,
[]string{"iso-grpC"},
nil,
true,
userID,
false,
false,
)
require.NoError(t, err)
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
// Updating policy group (iso-grpA) should affect peer1+peer2 but NOT peer3
t.Run("policy group change does not affect route-only peer", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
err := manager.UpdateGroup(ctx, accountID, userID, &types.Group{
ID: "iso-grpA",
Name: "ISO-A-updated",
Peers: []string{peer1.ID},
})
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_DeleteRoute_UnrelatedPeerNoUpdate verifies that deleting a route
// only sends updates to peers in the route's groups.
func TestAffectedPeers_DeleteRoute_UnrelatedPeerNoUpdate(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
for _, g := range []*types.Group{
{ID: "del-rt-grpA", Name: "Del-Rt-A", Peers: []string{peer1.ID}},
{ID: "del-rt-grpB", Name: "Del-Rt-B", Peers: []string{peer2.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
newRoute, err := manager.CreateRoute(ctx, accountID,
netip.MustParsePrefix("10.30.0.0/24"),
route.IPv4Network,
nil,
"",
[]string{"del-rt-grpA"},
"deletable route",
"delnet",
false,
9999,
[]string{"del-rt-grpB"},
nil,
true,
userID,
false,
false,
)
require.NoError(t, err)
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
t.Run("delete route only affects linked peers", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
err := manager.DeleteRoute(ctx, accountID, newRoute.ID, userID)
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_DeletePolicy_UnrelatedPeerNoUpdate verifies that deleting a policy
// only sends updates to peers in the policy's groups.
func TestAffectedPeers_DeletePolicy_UnrelatedPeerNoUpdate(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
for _, g := range []*types.Group{
{ID: "del-pol-grpA", Name: "Del-Pol-A", Peers: []string{peer1.ID}},
{ID: "del-pol-grpB", Name: "Del-Pol-B", Peers: []string{peer2.ID}},
} {
err := manager.CreateGroup(ctx, accountID, userID, g)
require.NoError(t, err)
}
policy, err := manager.SavePolicy(ctx, accountID, userID, &types.Policy{
Enabled: true,
Rules: []*types.PolicyRule{
{
Enabled: true,
Sources: []string{"del-pol-grpA"},
Destinations: []string{"del-pol-grpB"},
Bidirectional: true,
Action: types.PolicyTrafficActionAccept,
},
},
}, true)
require.NoError(t, err)
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
t.Run("delete policy only affects linked peers", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
err := manager.DeletePolicy(ctx, accountID, policy.ID, userID)
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
// TestAffectedPeers_DeleteNameServer_UnrelatedPeerNoUpdate verifies that deleting a
// nameserver group only sends updates to peers in its groups.
func TestAffectedPeers_DeleteNameServer_UnrelatedPeerNoUpdate(t *testing.T) {
manager, updateManager, account, peer1, peer2, peer3 := setupNetworkMapTest(t)
ctx := context.Background()
accountID := account.Id
policies, err := manager.Store.GetAccountPolicies(ctx, store.LockingStrengthNone, accountID)
require.NoError(t, err)
for _, p := range policies {
err := manager.Store.DeletePolicy(ctx, accountID, p.ID)
require.NoError(t, err)
}
err = manager.CreateGroup(ctx, accountID, userID, &types.Group{
ID: "del-ns-grpA",
Name: "Del-NS-A",
Peers: []string{peer1.ID},
})
require.NoError(t, err)
nsGroup, err := manager.CreateNameServerGroup(ctx, accountID, "del-ns", "Del NS",
[]nbdns.NameServer{{
IP: netip.MustParseAddr("8.8.4.4"),
NSType: nbdns.UDPNameServerType,
Port: nbdns.DefaultDNSPort,
}},
[]string{"del-ns-grpA"},
true, nil, true, userID, false,
)
require.NoError(t, err)
updMsg1 := updateManager.CreateChannel(ctx, peer1.ID)
updMsg2 := updateManager.CreateChannel(ctx, peer2.ID)
updMsg3 := updateManager.CreateChannel(ctx, peer3.ID)
t.Cleanup(func() {
updateManager.CloseChannel(ctx, peer1.ID)
updateManager.CloseChannel(ctx, peer2.ID)
updateManager.CloseChannel(ctx, peer3.ID)
})
t.Run("delete nameserver group only affects linked peers", func(t *testing.T) {
done := make(chan struct{})
go func() {
peerShouldReceiveUpdate(t, updMsg1)
peerShouldNotReceiveUpdate(t, updMsg2)
peerShouldNotReceiveUpdate(t, updMsg3)
close(done)
}()
err := manager.DeleteNameServerGroup(ctx, accountID, nsGroup.ID, userID)
assert.NoError(t, err)
select {
case <-done:
case <-time.After(peerUpdateTimeout):
t.Error("timeout")
}
})
}
func addPeerToAccount(t *testing.T, manager *DefaultAccountManager, _, setupKeyKey string) *nbpeer.Peer {
t.Helper()
key, err := wgtypes.GeneratePrivateKey()
require.NoError(t, err)
peer, _, _, err := manager.AddPeer(context.Background(), "", setupKeyKey, "", &nbpeer.Peer{
Key: key.PublicKey().String(),
Meta: nbpeer.PeerSystemMeta{Hostname: key.PublicKey().String()},
}, false)
require.NoError(t, err)
return peer
}