Skip to content

Commit

Permalink
find optimal prefix to allocate to avoid fragmentation
Browse files Browse the repository at this point in the history
Given two sets, the first one representing the full IP space which can
be used, and a second representing what has already been used or
allocated, this algorthim can find a best fit prefix that you can add to
reserved set which will minimize the fragmentation of the resulting
reserved set.

This can be used to allocate IP address subnets in the best way possible
to avoid situations where a subnet cannot be allocated because the space
was not allocated carefully enough.

It cannot prevent all fragmentation. Since it has no knowledge of how
prefixes may be deallocated in the future, it cannot make decisions to
allocate so that deallocations will open up the most contiguous space
possible.
  • Loading branch information
ecbaldwin committed Jun 3, 2024
1 parent 7c917bb commit 83c200e
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 0 deletions.
18 changes: 18 additions & 0 deletions ipv4/set.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ipv4

import (
"fmt"
"strings"
)

Expand Down Expand Up @@ -316,3 +317,20 @@ func (me Set) Difference(other SetI) Set {
func (me Set) isValid() bool {
return me.trie.isValid()
}

// FindPrefixWithLength returns a Prefix with a Mask of the given prefix length
// that is contained by the current set but does not overlap the given reserved
// set. The returned Prefix is optimally placed to avoid any further IP space
// fragmentation. An error is returned if there is not enough space to allocate
func (me Set) FindPrefixWithLength(reserved SetI, length uint32) (Prefix, error) {
diff := me.Difference(reserved)
prefix, err := diff.trie.FindSmallestContainingPrefix(length)
if err != nil {
return Prefix{}, fmt.Errorf("no room for prefix of given length")
}

return Prefix{
addr: prefix.Network().addr,
length: length,
}, nil
}
160 changes: 160 additions & 0 deletions ipv4/set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -860,3 +860,163 @@ func TestOldEqualAllIPv4(t *testing.T) {
assert.True(t, c.Equal(a))
assert.True(t, c.Equal(b))
}

func TestFindPrefixWithLength(t *testing.T) {
tests := []struct {
description string
space []SetI
reserved []SetI
length uint32
expected Prefix
err bool
change int
}{
{
description: "empty",
space: []SetI{
_p("10.0.0.0/8"),
},
length: 24,
change: 1,
}, {
description: "find adjacent",
space: []SetI{
_p("10.0.0.0/8"),
},
reserved: []SetI{
_p("10.224.123.0/24"),
},
length: 24,
expected: _p("10.224.122.0/24"),
}, {
description: "many fewer prefixes",
space: []SetI{
_p("10.0.0.0/16"),
},
reserved: []SetI{
_p("10.0.1.0/24"),
_p("10.0.2.0/23"),
_p("10.0.4.0/22"),
_p("10.0.8.0/21"),
_p("10.0.16.0/20"),
_p("10.0.32.0/19"),
_p("10.0.64.0/18"),
_p("10.0.128.0/17"),
},
length: 24,
change: -7,
}, {
description: "toobig",
space: []SetI{
_p("10.0.0.0/8"),
},
reserved: []SetI{
_p("10.128.0.0/9"),
_p("10.64.0.0/10"),
_p("10.32.0.0/11"),
_p("10.16.0.0/12"),
},
length: 11,
err: true,
}, {
description: "full",
space: []SetI{
_p("10.0.0.0/8"),
},
length: 7,
err: true,
}, {
description: "random disjoint example",
space: []SetI{
_p("10.0.0.0/22"),
_p("192.168.0.0/21"),
_p("172.16.0.0/20"),
},
reserved: []SetI{
_p("192.168.0.0/21"),
_p("172.16.0.0/21"),
_p("172.16.8.0/22"),
_p("10.0.0.0/22"),
_p("172.16.12.0/24"),
_p("172.16.14.0/24"),
_p("172.16.15.0/24"),
},
length: 24,
expected: _p("172.16.13.0/24"),
change: 1,
}, {
description: "too fragmented",
space: []SetI{
_p("10.0.0.0/24"),
},
reserved: []SetI{
_p("10.0.0.0/27"),
_p("10.0.0.64/27"),
_p("10.0.0.128/27"),
_p("10.0.0.192/27"),
},
length: 25,
err: true,
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
// This is the full usable IP space
space := Set{}.Build(func(s_ Set_) bool {
for _, p := range tt.space {
s_.Insert(p)
}
return true
})
// This is the part of the usable space which has already been allocated
reserved := Set{}.Build(func(s_ Set_) bool {
for _, p := range tt.reserved {
s_.Insert(p)
}
return true
})

// Call the method under test to find the best allocation to avoid fragmentation.
prefix, err := space.FindPrefixWithLength(reserved, tt.length)

assert.Equal(t, tt.err, err != nil)
if err != nil {
return
}

assert.Equal(t, int64(0), reserved.Intersection(prefix).NumAddresses())

// Not all test cases care which prefix is returned but in some
// cases, there is only one right answer and so we might check it.
// This isn't strictly necessary but was handy with the first few.
if tt.expected.length != 0 {
assert.Equal(t, tt.expected.String(), prefix.String())
}

// What really matters is that fragmentation in the IP space is
// always avoided as much as possible. The `change` field in each
// test indicates what should happen to IP space fragmentation.
// This test framework measures fragmentation as the change in the
// minimal number of prefixes required to span the reserved set.
before := countPrefixes(reserved)
after := countPrefixes(reserved.Build(func(s_ Set_) bool {
s_.Insert(prefix)
return true
}))

diff := after - before
assert.LessOrEqual(t, diff, 1)
assert.LessOrEqual(t, diff, tt.change)
})
}
}

func countPrefixes(s Set) int {
var numPrefixes int
s.WalkPrefixes(func(_ Prefix) bool {
numPrefixes += 1
return true
})
return numPrefixes
}
33 changes: 33 additions & 0 deletions ipv4/setnode.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ipv4

import (
"fmt"
"math/bits"
)

Expand Down Expand Up @@ -296,3 +297,35 @@ func (me *setNode) height() int {
func (me *setNode) Walk(callback func(Prefix, interface{}) bool) bool {
return (*trieNode)(me).Walk(callback)
}

func (me *setNode) FindSmallestContainingPrefix(length uint32) (Prefix, error) {
if me == nil || length < me.Prefix.length {
return Prefix{}, fmt.Errorf("cannot find containing prefix")
}
if length == me.Prefix.length {
if me.isActive {
return me.Prefix, nil
}
}

l, r := (*setNode)(me.children[0]), (*setNode)(me.children[1])
lPrefix, lErr := l.FindSmallestContainingPrefix(length)
rPrefix, rErr := r.FindSmallestContainingPrefix(length)
switch {
case lErr == nil && rErr == nil:
if lPrefix.length < rPrefix.length {
return rPrefix, nil
} else {
return lPrefix, nil
}
case lErr == nil:
return lPrefix, nil
case rErr == nil:
return rPrefix, nil
default:
if !me.isActive {
return Prefix{}, fmt.Errorf("cannot find containing prefix")
}
return me.Prefix, nil
}
}

0 comments on commit 83c200e

Please sign in to comment.