Skip to content

Commit

Permalink
Optimize ACL (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
YairSlobodin1 authored Jan 5, 2025
1 parent 9d8266c commit dea21da
Show file tree
Hide file tree
Showing 25 changed files with 3,317 additions and 905 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.23.0
require (
github.com/IBM/vpc-go-sdk v0.64.0
github.com/np-guard/cloud-resource-collector v0.17.0
github.com/np-guard/models v0.5.3
github.com/np-guard/models v0.5.4
github.com/spf13/cobra v1.8.1
)

Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/np-guard/cloud-resource-collector v0.17.0 h1:QFXrFeF9ZS3P4B2dnZNvpWXLUq/QGPuwDGTjarByCvs=
github.com/np-guard/cloud-resource-collector v0.17.0/go.mod h1:vp82iqdSq12EZJxA11oZkVseFVKg0hvPIUA9tVEdnMc=
github.com/np-guard/models v0.5.3 h1:XtpLNTyhU1ptmFrYL3MCZestEvRa0uyiYV7YptIeE/k=
github.com/np-guard/models v0.5.3/go.mod h1:dqRdt5EQID1GmHuYsMOJzg4sS104om6NwEZ6sVO55z8=
github.com/np-guard/models v0.5.4 h1:AoIu+XqQ6cFt67PkaD8NTPjxtDwz4pU2u9ptCrVPImU=
github.com/np-guard/models v0.5.4/go.mod h1:Cj2nHAECvIuNoEBHXyzvauf3jXYgLzjxuHH73COaazE=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand Down Expand Up @@ -181,8 +181,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
Expand Down
4 changes: 3 additions & 1 deletion pkg/io/commonACL.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,12 @@ func printIP(ip *netset.IPBlock, protocol netp.Protocol, isSource bool) (string,
return ipString, nil
case netp.TCPUDP:
r := p.DstPorts()
portsString := "dst ports:"
if isSource {
r = p.SrcPorts()
portsString = "src ports:"
}
return fmt.Sprintf("%v, %v", ipString, printPorts(r)), nil
return fmt.Sprintf("%v, %s %v", ipString, portsString, printPorts(r)), nil
case netp.AnyProtocol:
return ipString, nil
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/ir/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ func NewACL(aclName, subnetName string) *ACL {
return &ACL{Name: aclName, Subnets: []string{subnetName}}
}

func NewACLRule(action Action, direction Direction, src, dst *netset.IPBlock, p netp.Protocol, e string) *ACLRule {
return &ACLRule{Action: action, Direction: direction, Source: src, Destination: dst, Protocol: p, Explanation: e}
}

func aclSelector(subnetName ID, single bool) string {
if single {
return fmt.Sprintf("%s/singleACL", VpcFromScopedResource(subnetName))
Expand Down
77 changes: 77 additions & 0 deletions pkg/optimize/acl/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ SPDX-License-Identifier: Apache-2.0
package acloptimizer

import (
"fmt"
"log"

"github.com/np-guard/models/pkg/ds"
"github.com/np-guard/models/pkg/netset"
"github.com/np-guard/vpc-network-config-synthesis/pkg/ir"
"github.com/np-guard/vpc-network-config-synthesis/pkg/optimize"
"github.com/np-guard/vpc-network-config-synthesis/pkg/utils"
)

type (
Expand All @@ -16,6 +22,25 @@ type (
aclName string
aclVPC string
}

tcpudpTripleSet = ds.TripleSet[*netset.IPBlock, *netset.IPBlock, *netset.TCPUDPSet]
icmpTripleSet = ds.TripleSet[*netset.IPBlock, *netset.IPBlock, *netset.ICMPSet]
srcDstProduct = ds.Product[*netset.IPBlock, *netset.IPBlock]
srcDstProductLeft = ds.ProductLeft[*netset.IPBlock, *netset.IPBlock]

aclCubesPerProtocol struct {
tcpAllow tcpudpTripleSet
tcpDeny tcpudpTripleSet

udpAllow tcpudpTripleSet
udpDeny tcpudpTripleSet

icmpAllow icmpTripleSet
icmpDeny icmpTripleSet

// initialized in reduceCubes func
anyProtocolAllow srcDstProduct
}
)

func NewACLOptimizer(collection ir.Collection, aclName string) optimize.Optimizer {
Expand All @@ -27,5 +52,57 @@ func NewACLOptimizer(collection ir.Collection, aclName string) optimize.Optimize
}

func (a *aclOptimizer) Optimize() (ir.Collection, error) {
if a.aclName != "" {
for _, vpcName := range utils.SortedMapKeys(a.aclCollection.ACLs) {
if a.aclVPC != "" && a.aclVPC != vpcName {
continue
}
if _, ok := a.aclCollection.ACLs[vpcName][a.aclName]; ok {
a.optimizeACL(vpcName, a.aclName)
return a.aclCollection, nil
}
}
return nil, fmt.Errorf("could not find nACL %s", a.aclName)
}

for _, vpcName := range utils.SortedMapKeys(a.aclCollection.ACLs) {
for _, aclName := range utils.SortedMapKeys(a.aclCollection.ACLs[vpcName]) {
a.optimizeACL(vpcName, aclName)
}
}
return a.aclCollection, nil
}

func (a *aclOptimizer) optimizeACL(vpcName, aclName string) {
acl := a.aclCollection.ACLs[vpcName][aclName]
reducedRules := 0

// reduce inbound rules first
newInboundRules := a.reduceACLRules(acl.Inbound, ir.Inbound)
if len(acl.Inbound) > len(newInboundRules) {
reducedRules += len(acl.Inbound) - len(newInboundRules)
acl.Inbound = newInboundRules
}

// reduce outbound rules second
newOutboundRules := a.reduceACLRules(acl.Outbound, ir.Outbound)
if len(acl.Outbound) > len(newOutboundRules) {
reducedRules += len(acl.Outbound) - len(newOutboundRules)
acl.Outbound = newOutboundRules
}

// print a message to the log
if reducedRules == 0 {
log.Printf("no rules were reduced in acl %s\n", a.aclName)
} else {
log.Printf("the number of rules in acl %s was reduced by %d\n", a.aclName, reducedRules)
}
}

func (a *aclOptimizer) reduceACLRules(rules []*ir.ACLRule, direction ir.Direction) []*ir.ACLRule {
optimizedRules := aclCubesToRules(aclRulesToCubes(rules), direction)
if len(rules) > len(optimizedRules) {
return optimizedRules
}
return rules
}
91 changes: 91 additions & 0 deletions pkg/optimize/acl/cubesToRules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
Copyright 2023- IBM Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package acloptimizer

import (
"slices"

"github.com/np-guard/models/pkg/ds"
"github.com/np-guard/models/pkg/netp"
"github.com/np-guard/models/pkg/netset"
"github.com/np-guard/vpc-network-config-synthesis/pkg/ir"
"github.com/np-guard/vpc-network-config-synthesis/pkg/optimize"
)

func aclCubesToRules(cubes *aclCubesPerProtocol, direction ir.Direction) []*ir.ACLRule {
reduceACLCubes(cubes)
tcpRules := tcpudpTriplesToRules(cubes.tcpAllow, direction)
udpRules := tcpudpTriplesToRules(cubes.udpAllow, direction)
icmpRules := icmpTriplesToRules(cubes.icmpAllow, direction)
anyProtocolRules := anyProtocolCubesToRules(cubes.anyProtocolAllow, direction)
return slices.Concat(tcpRules, udpRules, icmpRules, anyProtocolRules)
}

// same algorithm as sg cubes to rules
func anyProtocolCubesToRules(cubes srcDstProduct, direction ir.Direction) []*ir.ACLRule {
partitions := optimize.SortPartitionsByIPAddrs(cubes.Partitions())
if len(partitions) == 0 {
return []*ir.ACLRule{}
}

res := make([]*ir.ACLRule, 0)
activeRules := make([]ds.Pair[*netset.IPBlock, *netset.IPBlock], 0) // Left = first src's IP, Right = dst cidr

for i := range partitions {
// if it is not possible to continue the rule between the cubes, generate all existing rules
if i > 0 && uncoveredHole(partitions[i-1].Left, partitions[i].Left) {
res = slices.Concat(res, createActiveRules(activeRules, partitions[i-1].Left.LastIPAddressObject(), direction))
activeRules = make([]ds.Pair[*netset.IPBlock, *netset.IPBlock], 0)
}

// if there are active rules whose dsts are not fully included in the current cube, they will be created
// also activeDstIPs will be calculated, which is the dstIPs that are still included in the active rules
activeDstIPs := netset.NewIPBlock()
for j, rule := range slices.Backward(activeRules) {
if rule.Right.IsSubset(partitions[i].Right) {
activeDstIPs = activeDstIPs.Union(rule.Right)
} else {
res = createNewRules(rule.Left, partitions[i-1].Left.LastIPAddressObject(), rule.Right, direction) // create active rule
activeRules = slices.Delete(activeRules, j, j+1)
}
}

// if the current cube contains dstIPs that are not contained in active rules, new rules will be created
for _, dstCidr := range partitions[i].Right.SplitToCidrs() {
if !dstCidr.IsSubset(activeDstIPs) {
rule := ds.Pair[*netset.IPBlock, *netset.IPBlock]{Left: partitions[i].Left.FirstIPAddressObject(), Right: dstCidr}
activeRules = append(activeRules, rule)
}
}
}
// generate all existing rules
return slices.Concat(res, createActiveRules(activeRules, partitions[len(partitions)-1].Left.LastIPAddressObject(), direction))
}

func createActiveRules(activeRules []ds.Pair[*netset.IPBlock, *netset.IPBlock], srcLastIP *netset.IPBlock,
direction ir.Direction) []*ir.ACLRule {
res := make([]*ir.ACLRule, 0)
for _, rule := range activeRules {
res = slices.Concat(res, createNewRules(rule.Left, srcLastIP, rule.Right, direction))
}
return res
}

func createNewRules(srcStartIP, srcEndIP, dstCidr *netset.IPBlock, direction ir.Direction) []*ir.ACLRule {
src, _ := netset.IPBlockFromIPRange(srcStartIP, srcEndIP)
srcCidrs := src.SplitToCidrs()

res := make([]*ir.ACLRule, len(srcCidrs))
for i, srcCidr := range srcCidrs {
res[i] = ir.NewACLRule(ir.Allow, direction, srcCidr, dstCidr, netp.AnyProtocol{}, "")
}
return res
}

func uncoveredHole(prevSrcIP, currSrcIP *netset.IPBlock) bool {
touching, _ := prevSrcIP.TouchingIPRanges(currSrcIP)
return !touching
}
67 changes: 67 additions & 0 deletions pkg/optimize/acl/icmpCubesToRules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright 2023- IBM Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package acloptimizer

import (
"slices"

"github.com/np-guard/models/pkg/ds"
"github.com/np-guard/models/pkg/netp"
"github.com/np-guard/models/pkg/netset"
"github.com/np-guard/vpc-network-config-synthesis/pkg/ir"
"github.com/np-guard/vpc-network-config-synthesis/pkg/optimize"
)

func icmpTriplesToRules(tripleSet icmpTripleSet, direction ir.Direction) []*ir.ACLRule {
partitions := minimalPartitionsICMP(tripleSet)
res := make([]*ir.ACLRule, len(partitions))
for i, t := range partitions {
res[i] = ir.NewACLRule(ir.Allow, direction, t.S1, t.S2, t.S3, "")
}
return res
}

func minimalPartitionsICMP(t icmpTripleSet) []ds.Triple[*netset.IPBlock, *netset.IPBlock, netp.ICMP] {
leftPartitions := actualPartitionsICMP(ds.AsLeftTripleSet(t))
outerPartitions := actualPartitionsICMP(ds.AsOuterTripleSet(t))
rightPartitions := actualPartitionsICMP(ds.AsRightTripleSet(t))

switch {
case len(leftPartitions) <= len(outerPartitions) && len(leftPartitions) <= len(rightPartitions):
return leftPartitions
case len(outerPartitions) <= len(leftPartitions) && len(outerPartitions) <= len(rightPartitions):
return outerPartitions
default:
return rightPartitions
}
}

func actualPartitionsICMP(t icmpTripleSet) []ds.Triple[*netset.IPBlock, *netset.IPBlock, netp.ICMP] {
res := make([]ds.Triple[*netset.IPBlock, *netset.IPBlock, netp.ICMP], 0)
for _, p := range t.Partitions() {
res = slices.Concat(res, breakICMPTriple(p))
}
return res
}

// break multi-cube to regular cube
func breakICMPTriple(t ds.Triple[*netset.IPBlock, *netset.IPBlock, *netset.ICMPSet]) []ds.Triple[*netset.IPBlock,
*netset.IPBlock, netp.ICMP] {
res := make([]ds.Triple[*netset.IPBlock, *netset.IPBlock, netp.ICMP], 0)

dstCidrs := t.S2.SplitToCidrs()
icmpPartitions := optimize.IcmpsetPartitions(t.S3)

for _, src := range t.S1.SplitToCidrs() {
for _, dst := range dstCidrs {
for _, icmp := range icmpPartitions {
a := ds.Triple[*netset.IPBlock, *netset.IPBlock, netp.ICMP]{S1: src, S2: dst, S3: icmp}
res = append(res, a)
}
}
}
return res
}
60 changes: 60 additions & 0 deletions pkg/optimize/acl/reduceCubes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
Copyright 2023- IBM Inc. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/

package acloptimizer

import (
"github.com/np-guard/models/pkg/ds"
"github.com/np-guard/models/pkg/netset"
)

// reduceACLCubes unifies a (src ip x dst ip) cube, separately allowed for tcp, udp and icmp, into one "any" cube
// (assuming all ports, codes, types)
func reduceACLCubes(aclCubes *aclCubesPerProtocol) {
allTCP := allTCPUDP(aclCubes.tcpAllow)
allUDP := allTCPUDP(aclCubes.udpAllow)
allicmp := allICMP(aclCubes.icmpAllow)
aclCubes.anyProtocolAllow = allTCP.Intersect(allUDP).Intersect(allicmp)
subtractAnyProtocolCubes(aclCubes)
}

func allTCPUDP(tcpudpAllow ds.TripleSet[*netset.IPBlock, *netset.IPBlock, *netset.TCPUDPSet]) *srcDstProductLeft {
res := ds.NewProductLeft[*netset.IPBlock, *netset.IPBlock]()
allTCPSet := netset.NewAllTCPOnlySet()
allUDPSet := netset.NewAllUDPOnlySet()
for _, p := range tcpudpAllow.Partitions() {
if p.S3.Equal(allTCPSet) || p.S3.Equal(allUDPSet) { // all tcp or udp ports
r := ds.CartesianPairLeft(p.S1, p.S2)
res = res.Union(r).(*srcDstProductLeft)
}
}
return res
}

func allICMP(icmpAllow ds.TripleSet[*netset.IPBlock, *netset.IPBlock, *netset.ICMPSet]) *srcDstProductLeft {
res := ds.NewProductLeft[*netset.IPBlock, *netset.IPBlock]()
for _, p := range icmpAllow.Partitions() {
if p.S3.IsAll() { // all icmp types and codes
r := ds.CartesianPairLeft(p.S1, p.S2)
res = res.Union(r).(*srcDstProductLeft)
}
}
return res
}

func subtractAnyProtocolCubes(aclCubes *aclCubesPerProtocol) {
allTcpudp := ds.NewLeftTripleSet[*netset.IPBlock, *netset.IPBlock, *netset.TCPUDPSet]()
allIcmp := ds.NewLeftTripleSet[*netset.IPBlock, *netset.IPBlock, *netset.ICMPSet]()
for _, p := range aclCubes.anyProtocolAllow.Partitions() {
t := ds.CartesianLeftTriple(p.Left, p.Right, netset.AllTCPUDPSet())
allTcpudp = allTcpudp.Union(t).(*ds.LeftTripleSet[*netset.IPBlock, *netset.IPBlock, *netset.TCPUDPSet])
i := ds.CartesianLeftTriple(p.Left, p.Right, netset.AllICMPSet())
allIcmp = allIcmp.Union(i).(*ds.LeftTripleSet[*netset.IPBlock, *netset.IPBlock, *netset.ICMPSet])
}

aclCubes.tcpAllow = aclCubes.tcpAllow.Subtract(allTcpudp)
aclCubes.udpAllow = aclCubes.udpAllow.Subtract(allTcpudp)
aclCubes.icmpAllow = aclCubes.icmpAllow.Subtract(allIcmp)
}
Loading

0 comments on commit dea21da

Please sign in to comment.