netbird/management/server/policy_test.go

827 lines
22 KiB
Go

package server
import (
"context"
"fmt"
"net"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/exp/slices"
nbgroup "github.com/netbirdio/netbird/management/server/group"
nbpeer "github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/posture"
)
func TestAccount_getPeersByPolicy(t *testing.T) {
account := &Account{
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
Status: &nbpeer.PeerStatus{},
},
"peerB": {
ID: "peerB",
IP: net.ParseIP("100.65.80.39"),
Status: &nbpeer.PeerStatus{},
},
"peerC": {
ID: "peerC",
IP: net.ParseIP("100.65.254.139"),
Status: &nbpeer.PeerStatus{},
},
"peerD": {
ID: "peerD",
IP: net.ParseIP("100.65.62.5"),
Status: &nbpeer.PeerStatus{},
},
"peerE": {
ID: "peerE",
IP: net.ParseIP("100.65.32.206"),
Status: &nbpeer.PeerStatus{},
},
"peerF": {
ID: "peerF",
IP: net.ParseIP("100.65.250.202"),
Status: &nbpeer.PeerStatus{},
},
"peerG": {
ID: "peerG",
IP: net.ParseIP("100.65.13.186"),
Status: &nbpeer.PeerStatus{},
},
"peerH": {
ID: "peerH",
IP: net.ParseIP("100.65.29.55"),
Status: &nbpeer.PeerStatus{},
},
},
Groups: map[string]*nbgroup.Group{
"GroupAll": {
ID: "GroupAll",
Name: "All",
Peers: []string{
"peerB",
"peerA",
"peerD",
"peerC",
"peerE",
"peerF",
"peerG",
"peerH",
},
},
"GroupSwarm": {
ID: "GroupSwarm",
Name: "swarm",
Peers: []string{
"peerB",
"peerA",
"peerD",
"peerE",
"peerG",
"peerH",
},
},
},
Policies: []*Policy{
{
ID: "RuleDefault",
Name: "Default",
Description: "This is a default rule that allows connections between all the resources",
Enabled: true,
Rules: []*PolicyRule{
{
ID: "RuleDefault",
Name: "Default",
Description: "This is a default rule that allows connections between all the resources",
Bidirectional: true,
Enabled: true,
Protocol: PolicyRuleProtocolALL,
Action: PolicyTrafficActionAccept,
Sources: []string{
"GroupAll",
},
Destinations: []string{
"GroupAll",
},
},
},
},
{
ID: "RuleSwarm",
Name: "Swarm",
Description: "No description",
Enabled: true,
Rules: []*PolicyRule{
{
ID: "RuleSwarm",
Name: "Swarm",
Description: "No description",
Bidirectional: true,
Enabled: true,
Protocol: PolicyRuleProtocolALL,
Action: PolicyTrafficActionAccept,
Sources: []string{
"GroupSwarm",
"GroupAll",
},
Destinations: []string{
"GroupSwarm",
},
},
},
},
},
}
validatedPeers := make(map[string]struct{})
for p := range account.Peers {
validatedPeers[p] = struct{}{}
}
t.Run("check that all peers get map", func(t *testing.T) {
for _, p := range account.Peers {
peers, firewallRules := account.getPeerConnectionResources(context.Background(), p.ID, validatedPeers)
assert.GreaterOrEqual(t, len(peers), 2, "minimum number peers should present")
assert.GreaterOrEqual(t, len(firewallRules), 2, "minimum number of firewall rules should present")
}
})
t.Run("check first peer map details", func(t *testing.T) {
peers, firewallRules := account.getPeerConnectionResources(context.Background(), "peerB", validatedPeers)
assert.Len(t, peers, 7)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerE"])
assert.Contains(t, peers, account.Peers["peerF"])
epectedFirewallRules := []*FirewallRule{
{
PeerIP: "0.0.0.0",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "0.0.0.0",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.14.88",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.14.88",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.254.139",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.254.139",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.62.5",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.62.5",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.32.206",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.32.206",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.250.202",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.250.202",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.13.186",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.13.186",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.29.55",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.29.55",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
}
assert.Len(t, firewallRules, len(epectedFirewallRules))
slices.SortFunc(epectedFirewallRules, sortFunc())
slices.SortFunc(firewallRules, sortFunc())
for i := range firewallRules {
assert.Equal(t, epectedFirewallRules[i], firewallRules[i])
}
})
}
func TestAccount_getPeersByPolicyDirect(t *testing.T) {
account := &Account{
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
Status: &nbpeer.PeerStatus{},
},
"peerB": {
ID: "peerB",
IP: net.ParseIP("100.65.80.39"),
Status: &nbpeer.PeerStatus{},
},
"peerC": {
ID: "peerC",
IP: net.ParseIP("100.65.254.139"),
Status: &nbpeer.PeerStatus{},
},
},
Groups: map[string]*nbgroup.Group{
"GroupAll": {
ID: "GroupAll",
Name: "All",
Peers: []string{
"peerB",
"peerA",
"peerC",
},
},
"GroupSwarm": {
ID: "GroupSwarm",
Name: "swarm",
Peers: []string{
"peerB",
},
},
"peerF": {
ID: "peerF",
Name: "dmz",
Peers: []string{
"peerC",
},
},
},
Policies: []*Policy{
{
ID: "RuleDefault",
Name: "Default",
Description: "This is a default rule that allows connections between all the resources",
Enabled: false,
Rules: []*PolicyRule{
{
ID: "RuleDefault",
Name: "Default",
Description: "This is a default rule that allows connections between all the resources",
Bidirectional: true,
Enabled: false,
Protocol: PolicyRuleProtocolALL,
Action: PolicyTrafficActionAccept,
Sources: []string{
"GroupAll",
},
Destinations: []string{
"GroupAll",
},
},
},
},
{
ID: "RuleSwarm",
Name: "Swarm",
Description: "No description",
Enabled: true,
Rules: []*PolicyRule{
{
ID: "RuleSwarm",
Name: "Swarm",
Description: "No description",
Bidirectional: true,
Enabled: true,
Protocol: PolicyRuleProtocolALL,
Action: PolicyTrafficActionAccept,
Sources: []string{
"GroupSwarm",
},
Destinations: []string{
"peerF",
},
},
},
},
},
}
approvedPeers := make(map[string]struct{})
for p := range account.Peers {
approvedPeers[p] = struct{}{}
}
t.Run("check first peer map", func(t *testing.T) {
peers, firewallRules := account.getPeerConnectionResources(context.Background(), "peerB", approvedPeers)
assert.Contains(t, peers, account.Peers["peerC"])
epectedFirewallRules := []*FirewallRule{
{
PeerIP: "100.65.254.139",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.254.139",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
}
assert.Len(t, firewallRules, len(epectedFirewallRules))
slices.SortFunc(epectedFirewallRules, sortFunc())
slices.SortFunc(firewallRules, sortFunc())
for i := range firewallRules {
assert.Equal(t, epectedFirewallRules[i], firewallRules[i])
}
})
t.Run("check second peer map", func(t *testing.T) {
peers, firewallRules := account.getPeerConnectionResources(context.Background(), "peerC", approvedPeers)
assert.Contains(t, peers, account.Peers["peerB"])
epectedFirewallRules := []*FirewallRule{
{
PeerIP: "100.65.80.39",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
{
PeerIP: "100.65.80.39",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
}
assert.Len(t, firewallRules, len(epectedFirewallRules))
slices.SortFunc(epectedFirewallRules, sortFunc())
slices.SortFunc(firewallRules, sortFunc())
for i := range firewallRules {
assert.Equal(t, epectedFirewallRules[i], firewallRules[i])
}
})
account.Policies[1].Rules[0].Bidirectional = false
t.Run("check first peer map directional only", func(t *testing.T) {
peers, firewallRules := account.getPeerConnectionResources(context.Background(), "peerB", approvedPeers)
assert.Contains(t, peers, account.Peers["peerC"])
epectedFirewallRules := []*FirewallRule{
{
PeerIP: "100.65.254.139",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "all",
Port: "",
},
}
assert.Len(t, firewallRules, len(epectedFirewallRules))
slices.SortFunc(epectedFirewallRules, sortFunc())
slices.SortFunc(firewallRules, sortFunc())
for i := range firewallRules {
assert.Equal(t, epectedFirewallRules[i], firewallRules[i])
}
})
t.Run("check second peer map directional only", func(t *testing.T) {
peers, firewallRules := account.getPeerConnectionResources(context.Background(), "peerC", approvedPeers)
assert.Contains(t, peers, account.Peers["peerB"])
epectedFirewallRules := []*FirewallRule{
{
PeerIP: "100.65.80.39",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "all",
Port: "",
},
}
assert.Len(t, firewallRules, len(epectedFirewallRules))
slices.SortFunc(epectedFirewallRules, sortFunc())
slices.SortFunc(firewallRules, sortFunc())
for i := range firewallRules {
assert.Equal(t, epectedFirewallRules[i], firewallRules[i])
}
})
}
func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) {
account := &Account{
Peers: map[string]*nbpeer.Peer{
"peerA": {
ID: "peerA",
IP: net.ParseIP("100.65.14.88"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.7",
WtVersion: "0.25.9",
},
},
"peerB": {
ID: "peerB",
IP: net.ParseIP("100.65.80.39"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.23.0",
},
},
"peerC": {
ID: "peerC",
IP: net.ParseIP("100.65.254.139"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.25.8",
},
},
"peerD": {
ID: "peerD",
IP: net.ParseIP("100.65.62.5"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.0",
WtVersion: "0.25.9",
},
},
"peerE": {
ID: "peerE",
IP: net.ParseIP("100.65.32.206"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.24.0",
},
},
"peerF": {
ID: "peerF",
IP: net.ParseIP("100.65.250.202"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.25.9",
},
},
"peerG": {
ID: "peerG",
IP: net.ParseIP("100.65.13.186"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.23.2",
},
},
"peerH": {
ID: "peerH",
IP: net.ParseIP("100.65.29.55"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "linux",
KernelVersion: "6.6.1",
WtVersion: "0.23.1",
},
},
"peerI": {
ID: "peerI",
IP: net.ParseIP("100.65.21.56"),
Status: &nbpeer.PeerStatus{},
Meta: nbpeer.PeerSystemMeta{
GoOS: "windows",
KernelVersion: "10.0.14393.2430",
WtVersion: "0.25.1",
},
},
},
Groups: map[string]*nbgroup.Group{
"GroupAll": {
ID: "GroupAll",
Name: "All",
Peers: []string{
"peerB",
"peerA",
"peerD",
"peerC",
"peerF",
"peerG",
"peerH",
"peerI",
},
},
"GroupSwarm": {
ID: "GroupSwarm",
Name: "swarm",
Peers: []string{
"peerB",
"peerA",
"peerD",
"peerE",
"peerG",
"peerH",
"peerI",
},
},
},
PostureChecks: []*posture.Checks{
{
ID: "PostureChecksDefault",
Name: "Default",
Description: "This is a posture checks that check if peer is running required versions",
Checks: posture.ChecksDefinition{
NBVersionCheck: &posture.NBVersionCheck{
MinVersion: "0.25",
},
OSVersionCheck: &posture.OSVersionCheck{
Linux: &posture.MinKernelVersionCheck{
MinKernelVersion: "6.6.0",
},
},
},
},
},
}
account.Policies = append(account.Policies, &Policy{
ID: "PolicyPostureChecks",
Name: "",
Description: "This is the policy with posture checks applied",
Enabled: true,
Rules: []*PolicyRule{
{
ID: "RuleSwarm",
Name: "Swarm",
Enabled: true,
Action: PolicyTrafficActionAccept,
Destinations: []string{
"GroupSwarm",
},
Sources: []string{
"GroupAll",
},
Bidirectional: false,
Protocol: PolicyRuleProtocolTCP,
Ports: []string{"80"},
},
},
SourcePostureChecks: []string{
"PostureChecksDefault",
},
})
approvedPeers := make(map[string]struct{})
for p := range account.Peers {
approvedPeers[p] = struct{}{}
}
t.Run("verify peer's network map with default group peer list", func(t *testing.T) {
// peerB doesn't fulfill the NB posture check but is included in the destination group Swarm,
// will establish a connection with all source peers satisfying the NB posture check.
peers, firewallRules := account.getPeerConnectionResources(context.Background(), "peerB", approvedPeers)
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerF"])
// peerC satisfy the NB posture check, should establish connection to all destination group peer's
// We expect a single permissive firewall rule which all outgoing connections
peers, firewallRules = account.getPeerConnectionResources(context.Background(), "peerC", approvedPeers)
assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers))
assert.Len(t, firewallRules, 1)
expectedFirewallRules := []*FirewallRule{
{
PeerIP: "0.0.0.0",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
}
assert.ElementsMatch(t, firewallRules, expectedFirewallRules)
// peerE doesn't fulfill the NB posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules = account.getPeerConnectionResources(context.Background(), "peerE", approvedPeers)
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerF"])
// peerI doesn't fulfill the OS version posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules = account.getPeerConnectionResources(context.Background(), "peerI", approvedPeers)
assert.Len(t, peers, 4)
assert.Len(t, firewallRules, 4)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerF"])
})
t.Run("verify peer's network map with modified group peer list", func(t *testing.T) {
// Removing peerB as the part of destination group Swarm
account.Groups["GroupSwarm"].Peers = []string{"peerA", "peerD", "peerE", "peerG", "peerH"}
// peerB doesn't satisfy the NB posture check, and doesn't exist in destination group peer's
// no connection should be established to any peer of destination group
peers, firewallRules := account.getPeerConnectionResources(context.Background(), "peerB", approvedPeers)
assert.Len(t, peers, 0)
assert.Len(t, firewallRules, 0)
// peerI doesn't satisfy the OS version posture check, and doesn't exist in destination group peer's
// no connection should be established to any peer of destination group
peers, firewallRules = account.getPeerConnectionResources(context.Background(), "peerI", approvedPeers)
assert.Len(t, peers, 0)
assert.Len(t, firewallRules, 0)
// peerC satisfy the NB posture check, should establish connection to all destination group peer's
// We expect a single permissive firewall rule which all outgoing connections
peers, firewallRules = account.getPeerConnectionResources(context.Background(), "peerC", approvedPeers)
assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers))
assert.Len(t, firewallRules, len(account.Groups["GroupSwarm"].Peers))
peerIDs := make([]string, 0, len(peers))
for _, peer := range peers {
peerIDs = append(peerIDs, peer.ID)
}
assert.ElementsMatch(t, peerIDs, account.Groups["GroupSwarm"].Peers)
// Removing peerF as the part of source group All
account.Groups["GroupAll"].Peers = []string{"peerB", "peerA", "peerD", "peerC", "peerG", "peerH"}
// peerE doesn't fulfill the NB posture check and exists in only destination group Swarm,
// all source group peers satisfying the NB posture check should establish connection
peers, firewallRules = account.getPeerConnectionResources(context.Background(), "peerE", approvedPeers)
assert.Len(t, peers, 3)
assert.Len(t, firewallRules, 3)
assert.Contains(t, peers, account.Peers["peerA"])
assert.Contains(t, peers, account.Peers["peerC"])
assert.Contains(t, peers, account.Peers["peerD"])
peers, firewallRules = account.getPeerConnectionResources(context.Background(), "peerA", approvedPeers)
assert.Len(t, peers, 5)
// assert peers from Group Swarm
assert.Contains(t, peers, account.Peers["peerD"])
assert.Contains(t, peers, account.Peers["peerE"])
assert.Contains(t, peers, account.Peers["peerG"])
assert.Contains(t, peers, account.Peers["peerH"])
// assert peers from Group All
assert.Contains(t, peers, account.Peers["peerC"])
expectedFirewallRules := []*FirewallRule{
{
PeerIP: "100.65.62.5",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.32.206",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.13.186",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.29.55",
Direction: firewallRuleDirectionOUT,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.254.139",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
{
PeerIP: "100.65.62.5",
Direction: firewallRuleDirectionIN,
Action: "accept",
Protocol: "tcp",
Port: "80",
},
}
assert.Len(t, firewallRules, len(expectedFirewallRules))
assert.ElementsMatch(t, firewallRules, expectedFirewallRules)
})
}
func sortFunc() func(a *FirewallRule, b *FirewallRule) int {
return func(a, b *FirewallRule) int {
// Concatenate PeerIP and Direction as string for comparison
aStr := a.PeerIP + fmt.Sprintf("%d", a.Direction)
bStr := b.PeerIP + fmt.Sprintf("%d", b.Direction)
// Compare the concatenated strings
if aStr < bStr {
return -1 // a is less than b
}
if aStr > bStr {
return 1 // a is greater than b
}
return 0 // a is equal to b
}
}