From 83c200ebe579b4b4775ce2690b6e6f36480a9f29 Mon Sep 17 00:00:00 2001 From: Carl Baldwin Date: Wed, 8 May 2024 15:30:24 -0600 Subject: [PATCH] find optimal prefix to allocate to avoid fragmentation 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. --- ipv4/set.go | 18 ++++++ ipv4/set_test.go | 160 +++++++++++++++++++++++++++++++++++++++++++++++ ipv4/setnode.go | 33 ++++++++++ 3 files changed, 211 insertions(+) diff --git a/ipv4/set.go b/ipv4/set.go index ae4fe5f..9ba8104 100644 --- a/ipv4/set.go +++ b/ipv4/set.go @@ -1,6 +1,7 @@ package ipv4 import ( + "fmt" "strings" ) @@ -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 +} diff --git a/ipv4/set_test.go b/ipv4/set_test.go index 3d57121..04ab3c7 100644 --- a/ipv4/set_test.go +++ b/ipv4/set_test.go @@ -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 +} diff --git a/ipv4/setnode.go b/ipv4/setnode.go index b950744..6681cd4 100644 --- a/ipv4/setnode.go +++ b/ipv4/setnode.go @@ -1,6 +1,7 @@ package ipv4 import ( + "fmt" "math/bits" ) @@ -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 + } +}