diff --git a/management/server/peer.go b/management/server/peer.go index f2469e09b..1a1289721 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -92,7 +92,7 @@ func (am *DefaultAccountManager) getUserAccessiblePeers(ctx context.Context, acc // fetch all the peers that have access to the user's peers for _, peer := range peers { - aclPeers, _ := account.GetPeerConnectionResources(ctx, peer.ID, approvedPeersMap) + aclPeers, _ := account.GetPeerConnectionResources(ctx, peer, approvedPeersMap) for _, p := range aclPeers { peersMap[p.ID] = p } @@ -1149,7 +1149,7 @@ func (am *DefaultAccountManager) checkIfUserOwnsPeer(ctx context.Context, accoun } for _, p := range userPeers { - aclPeers, _ := account.GetPeerConnectionResources(ctx, p.ID, approvedPeersMap) + aclPeers, _ := account.GetPeerConnectionResources(ctx, p, approvedPeersMap) for _, aclPeer := range aclPeers { if aclPeer.ID == peer.ID { return peer, nil diff --git a/management/server/policy_test.go b/management/server/policy_test.go index 679ec3b86..4352f3cff 100644 --- a/management/server/policy_test.go +++ b/management/server/policy_test.go @@ -27,6 +27,7 @@ func TestAccount_getPeersByPolicy(t *testing.T) { ID: "peerB", IP: net.ParseIP("100.65.80.39"), Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{WtVersion: "0.48.0"}, }, "peerC": { ID: "peerC", @@ -63,6 +64,12 @@ func TestAccount_getPeersByPolicy(t *testing.T) { IP: net.ParseIP("100.65.31.2"), Status: &nbpeer.PeerStatus{}, }, + "peerK": { + ID: "peerK", + IP: net.ParseIP("100.32.80.1"), + Status: &nbpeer.PeerStatus{}, + Meta: nbpeer.PeerSystemMeta{WtVersion: "0.30.0"}, + }, }, Groups: map[string]*types.Group{ "GroupAll": { @@ -111,6 +118,13 @@ func TestAccount_getPeersByPolicy(t *testing.T) { "peerI", }, }, + "GroupWorkflow": { + ID: "GroupWorkflow", + Name: "workflow", + Peers: []string{ + "peerK", + }, + }, }, Policies: []*types.Policy{ { @@ -189,6 +203,39 @@ func TestAccount_getPeersByPolicy(t *testing.T) { }, }, }, + { + ID: "RuleWorkflow", + Name: "Workflow", + Description: "No description", + Enabled: true, + Rules: []*types.PolicyRule{ + { + ID: "RuleWorkflow", + Name: "Workflow", + Description: "No description", + Bidirectional: true, + Enabled: true, + Protocol: types.PolicyRuleProtocolTCP, + Action: types.PolicyTrafficActionAccept, + PortRanges: []types.RulePortRange{ + { + Start: 8088, + End: 8088, + }, + { + Start: 9090, + End: 9095, + }, + }, + Sources: []string{ + "GroupWorkflow", + }, + Destinations: []string{ + "GroupDMZ", + }, + }, + }, + }, }, } @@ -199,14 +246,14 @@ func TestAccount_getPeersByPolicy(t *testing.T) { 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") + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), p, validatedPeers) + assert.GreaterOrEqual(t, len(peers), 1, "minimum number peers should present") + assert.GreaterOrEqual(t, len(firewallRules), 1, "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) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], validatedPeers) assert.Len(t, peers, 8) assert.Contains(t, peers, account.Peers["peerA"]) assert.Contains(t, peers, account.Peers["peerC"]) @@ -364,6 +411,32 @@ func TestAccount_getPeersByPolicy(t *testing.T) { assert.True(t, contains, "rule not found in expected rules %#v", rule) } }) + + t.Run("check port ranges support for older peers", func(t *testing.T) { + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerK"], validatedPeers) + assert.Len(t, peers, 1) + assert.Contains(t, peers, account.Peers["peerI"]) + + expectedFirewallRules := []*types.FirewallRule{ + { + PeerIP: "100.65.31.2", + Direction: types.FirewallRuleDirectionIN, + Action: "accept", + Protocol: "tcp", + Port: "8088", + PolicyID: "RuleWorkflow", + }, + { + PeerIP: "100.65.31.2", + Direction: types.FirewallRuleDirectionOUT, + Action: "accept", + Protocol: "tcp", + Port: "8088", + PolicyID: "RuleWorkflow", + }, + } + assert.ElementsMatch(t, firewallRules, expectedFirewallRules) + }) } func TestAccount_getPeersByPolicyDirect(t *testing.T) { @@ -466,10 +539,10 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { } t.Run("check first peer map", func(t *testing.T) { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerB", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers) assert.Contains(t, peers, account.Peers["peerC"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "100.65.254.139", Direction: types.FirewallRuleDirectionIN, @@ -487,19 +560,19 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { PolicyID: "RuleSwarm", }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(epectedFirewallRules, sortFunc()) + assert.Len(t, firewallRules, len(expectedFirewallRules)) + slices.SortFunc(expectedFirewallRules, sortFunc()) slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { - assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + assert.Equal(t, expectedFirewallRules[i], firewallRules[i]) } }) t.Run("check second peer map", func(t *testing.T) { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerC", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers) assert.Contains(t, peers, account.Peers["peerB"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "100.65.80.39", Direction: types.FirewallRuleDirectionIN, @@ -517,21 +590,21 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { PolicyID: "RuleSwarm", }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(epectedFirewallRules, sortFunc()) + assert.Len(t, firewallRules, len(expectedFirewallRules)) + slices.SortFunc(expectedFirewallRules, sortFunc()) slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { - assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + assert.Equal(t, expectedFirewallRules[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) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers) assert.Contains(t, peers, account.Peers["peerC"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "100.65.254.139", Direction: types.FirewallRuleDirectionOUT, @@ -541,19 +614,19 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { PolicyID: "RuleSwarm", }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(epectedFirewallRules, sortFunc()) + assert.Len(t, firewallRules, len(expectedFirewallRules)) + slices.SortFunc(expectedFirewallRules, sortFunc()) slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { - assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + assert.Equal(t, expectedFirewallRules[i], firewallRules[i]) } }) t.Run("check second peer map directional only", func(t *testing.T) { - peers, firewallRules := account.GetPeerConnectionResources(context.Background(), "peerC", approvedPeers) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers) assert.Contains(t, peers, account.Peers["peerB"]) - epectedFirewallRules := []*types.FirewallRule{ + expectedFirewallRules := []*types.FirewallRule{ { PeerIP: "100.65.80.39", Direction: types.FirewallRuleDirectionIN, @@ -563,11 +636,11 @@ func TestAccount_getPeersByPolicyDirect(t *testing.T) { PolicyID: "RuleSwarm", }, } - assert.Len(t, firewallRules, len(epectedFirewallRules)) - slices.SortFunc(epectedFirewallRules, sortFunc()) + assert.Len(t, firewallRules, len(expectedFirewallRules)) + slices.SortFunc(expectedFirewallRules, sortFunc()) slices.SortFunc(firewallRules, sortFunc()) for i := range firewallRules { - assert.Equal(t, epectedFirewallRules[i], firewallRules[i]) + assert.Equal(t, expectedFirewallRules[i], firewallRules[i]) } }) } @@ -748,7 +821,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { 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) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["peerB"], approvedPeers) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -758,7 +831,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // 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) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers) assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers)) assert.Len(t, firewallRules, 1) expectedFirewallRules := []*types.FirewallRule{ @@ -775,7 +848,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // 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) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerE"], approvedPeers) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -785,7 +858,7 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // 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) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerI"], approvedPeers) assert.Len(t, peers, 4) assert.Len(t, firewallRules, 4) assert.Contains(t, peers, account.Peers["peerA"]) @@ -800,19 +873,19 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // 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) + peers, firewallRules := account.GetPeerConnectionResources(context.Background(), account.Peers["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) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["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) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerC"], approvedPeers) assert.Len(t, peers, len(account.Groups["GroupSwarm"].Peers)) assert.Len(t, firewallRules, len(account.Groups["GroupSwarm"].Peers)) @@ -827,14 +900,14 @@ func TestAccount_getPeersByPolicyPostureChecks(t *testing.T) { // 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) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["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) + peers, firewallRules = account.GetPeerConnectionResources(context.Background(), account.Peers["peerA"], approvedPeers) assert.Len(t, peers, 5) // assert peers from Group Swarm assert.Contains(t, peers, account.Peers["peerD"]) diff --git a/management/server/posture/nb_version.go b/management/server/posture/nb_version.go index e98e8e795..33bf01ad1 100644 --- a/management/server/posture/nb_version.go +++ b/management/server/posture/nb_version.go @@ -24,20 +24,12 @@ func sanitizeVersion(version string) string { } func (n *NBVersionCheck) Check(ctx context.Context, peer nbpeer.Peer) (bool, error) { - peerVersion := sanitizeVersion(peer.Meta.WtVersion) - minVersion := sanitizeVersion(n.MinVersion) - - peerNBVersion, err := version.NewVersion(peerVersion) + meetsMin, err := MeetsMinVersion(n.MinVersion, peer.Meta.WtVersion) if err != nil { return false, err } - constraints, err := version.NewConstraint(">= " + minVersion) - if err != nil { - return false, err - } - - if constraints.Check(peerNBVersion) { + if meetsMin { return true, nil } @@ -60,3 +52,21 @@ func (n *NBVersionCheck) Validate() error { } return nil } + +// MeetsMinVersion checks if the peer's version meets or exceeds the minimum required version +func MeetsMinVersion(minVer, peerVer string) (bool, error) { + peerVer = sanitizeVersion(peerVer) + minVer = sanitizeVersion(minVer) + + peerNBVer, err := version.NewVersion(peerVer) + if err != nil { + return false, err + } + + constraints, err := version.NewConstraint(">= " + minVer) + if err != nil { + return false, err + } + + return constraints.Check(peerNBVer), nil +} diff --git a/management/server/posture/nb_version_test.go b/management/server/posture/nb_version_test.go index 1bf485453..d3478afc2 100644 --- a/management/server/posture/nb_version_test.go +++ b/management/server/posture/nb_version_test.go @@ -139,3 +139,68 @@ func TestNBVersionCheck_Validate(t *testing.T) { }) } } + +func TestMeetsMinVersion(t *testing.T) { + tests := []struct { + name string + minVer string + peerVer string + want bool + wantErr bool + }{ + { + name: "Peer version greater than min version", + minVer: "0.26.0", + peerVer: "0.60.1", + want: true, + wantErr: false, + }, + { + name: "Peer version equals min version", + minVer: "1.0.0", + peerVer: "1.0.0", + want: true, + wantErr: false, + }, + { + name: "Peer version less than min version", + minVer: "1.0.0", + peerVer: "0.9.9", + want: false, + wantErr: false, + }, + { + name: "Peer version with pre-release tag greater than min version", + minVer: "1.0.0", + peerVer: "1.0.1-alpha", + want: true, + wantErr: false, + }, + { + name: "Invalid peer version format", + minVer: "1.0.0", + peerVer: "dev", + want: false, + wantErr: true, + }, + { + name: "Invalid min version format", + minVer: "invalid.version", + peerVer: "1.0.0", + want: false, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := MeetsMinVersion(tt.minVer, tt.peerVer) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/management/server/types/account.go b/management/server/types/account.go index da230f0b2..090ba76e4 100644 --- a/management/server/types/account.go +++ b/management/server/types/account.go @@ -36,6 +36,9 @@ const ( PublicCategory = "public" PrivateCategory = "private" UnknownCategory = "unknown" + + // firewallRuleMinPortRangesVer defines the minimum peer version that supports port range rules. + firewallRuleMinPortRangesVer = "0.48.0" ) type LookupMap map[string]struct{} @@ -248,7 +251,7 @@ func (a *Account) GetPeerNetworkMap( } } - aclPeers, firewallRules := a.GetPeerConnectionResources(ctx, peerID, validatedPeersMap) + aclPeers, firewallRules := a.GetPeerConnectionResources(ctx, peer, validatedPeersMap) // exclude expired peers var peersToConnect []*nbpeer.Peer var expiredPeers []*nbpeer.Peer @@ -961,8 +964,9 @@ func (a *Account) UserGroupsRemoveFromPeers(userID string, groups ...string) map // GetPeerConnectionResources for a given peer // // This function returns the list of peers and firewall rules that are applicable to a given peer. -func (a *Account) GetPeerConnectionResources(ctx context.Context, peerID string, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, []*FirewallRule) { - generateResources, getAccumulatedResources := a.connResourcesGenerator(ctx) +func (a *Account) GetPeerConnectionResources(ctx context.Context, peer *nbpeer.Peer, validatedPeersMap map[string]struct{}) ([]*nbpeer.Peer, []*FirewallRule) { + generateResources, getAccumulatedResources := a.connResourcesGenerator(ctx, peer) + for _, policy := range a.Policies { if !policy.Enabled { continue @@ -973,8 +977,8 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peerID string, continue } - sourcePeers, peerInSources := a.getAllPeersFromGroups(ctx, rule.Sources, peerID, policy.SourcePostureChecks, validatedPeersMap) - destinationPeers, peerInDestinations := a.getAllPeersFromGroups(ctx, rule.Destinations, peerID, nil, validatedPeersMap) + sourcePeers, peerInSources := a.getAllPeersFromGroups(ctx, rule.Sources, peer.ID, policy.SourcePostureChecks, validatedPeersMap) + destinationPeers, peerInDestinations := a.getAllPeersFromGroups(ctx, rule.Destinations, peer.ID, nil, validatedPeersMap) if rule.Bidirectional { if peerInSources { @@ -1003,7 +1007,7 @@ func (a *Account) GetPeerConnectionResources(ctx context.Context, peerID string, // The generator function is used to generate the list of peers and firewall rules that are applicable to a given peer. // It safe to call the generator function multiple times for same peer and different rules no duplicates will be // generated. The accumulator function returns the result of all the generator calls. -func (a *Account) connResourcesGenerator(ctx context.Context) (func(*PolicyRule, []*nbpeer.Peer, int), func() ([]*nbpeer.Peer, []*FirewallRule)) { +func (a *Account) connResourcesGenerator(ctx context.Context, targetPeer *nbpeer.Peer) (func(*PolicyRule, []*nbpeer.Peer, int), func() ([]*nbpeer.Peer, []*FirewallRule)) { rulesExists := make(map[string]struct{}) peersExists := make(map[string]struct{}) rules := make([]*FirewallRule, 0) @@ -1051,17 +1055,7 @@ func (a *Account) connResourcesGenerator(ctx context.Context) (func(*PolicyRule, continue } - for _, port := range rule.Ports { - pr := fr // clone rule and add set new port - pr.Port = port - rules = append(rules, &pr) - } - - for _, portRange := range rule.PortRanges { - pr := fr - pr.PortRange = portRange - rules = append(rules, &pr) - } + rules = append(rules, expandPortsAndRanges(fr, rule, targetPeer)...) } }, func() ([]*nbpeer.Peer, []*FirewallRule) { return peers, rules @@ -1590,3 +1584,45 @@ func (a *Account) AddAllGroup() error { } return nil } + +// expandPortsAndRanges expands Ports and PortRanges of a rule into individual firewall rules +func expandPortsAndRanges(base FirewallRule, rule *PolicyRule, peer *nbpeer.Peer) []*FirewallRule { + var expanded []*FirewallRule + + if len(rule.Ports) > 0 { + for _, port := range rule.Ports { + fr := base + fr.Port = port + expanded = append(expanded, &fr) + } + return expanded + } + + supportPortRanges := peerSupportsPortRanges(peer.Meta.WtVersion) + for _, portRange := range rule.PortRanges { + fr := base + + if supportPortRanges { + fr.PortRange = portRange + } else { + // Peer doesn't support port ranges, only allow single-port ranges + if portRange.Start != portRange.End { + continue + } + fr.Port = strconv.FormatUint(uint64(portRange.Start), 10) + } + expanded = append(expanded, &fr) + } + + return expanded +} + +// peerSupportsPortRanges checks if the peer version supports port ranges. +func peerSupportsPortRanges(peerVer string) bool { + if strings.Contains(peerVer, "dev") { + return true + } + + meetMinVer, err := posture.MeetsMinVersion(firewallRuleMinPortRangesVer, peerVer) + return err == nil && meetMinVer +} diff --git a/management/server/types/firewall_rule.go b/management/server/types/firewall_rule.go index ef54abea2..19222a607 100644 --- a/management/server/types/firewall_rule.go +++ b/management/server/types/firewall_rule.go @@ -76,7 +76,6 @@ func generateRouteFirewallRules(ctx context.Context, route *nbroute.Route, rule rules = append(rules, generateRulesWithPortRanges(baseRule, rule, rulesExists)...) } else { rules = append(rules, generateRulesWithPorts(ctx, baseRule, rule, rulesExists)...) - } // TODO: generate IPv6 rules for dynamic routes