From f24ef41cfa00620094aa6d7cc8a053130ee15e5f Mon Sep 17 00:00:00 2001 From: Carl Baldwin Date: Wed, 8 May 2024 15:30:24 -0600 Subject: [PATCH 1/5] 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 faff324..86e8fc7 100644 --- a/ipv4/set.go +++ b/ipv4/set.go @@ -1,6 +1,7 @@ package ipv4 import ( + "fmt" "strings" ) @@ -339,3 +340,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 12f1798..62b1233 100644 --- a/ipv4/set_test.go +++ b/ipv4/set_test.go @@ -953,3 +953,163 @@ func TestSetNumPrefixesStairs(t *testing.T) { }) } } + +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 + } +} From e3342d6f626205c99db1acb59816d1aa47d6b1e4 Mon Sep 17 00:00:00 2001 From: Carl Baldwin Date: Mon, 3 Jun 2024 19:06:59 -0600 Subject: [PATCH 2/5] add a randomized fragmentation case --- ipv4/set_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/ipv4/set_test.go b/ipv4/set_test.go index 62b1233..6356562 100644 --- a/ipv4/set_test.go +++ b/ipv4/set_test.go @@ -1,6 +1,7 @@ package ipv4 import ( + "math" "math/rand" "strconv" "sync" @@ -1103,6 +1104,53 @@ func TestFindPrefixWithLength(t *testing.T) { assert.LessOrEqual(t, diff, tt.change) }) } + + t.Run("randomized", func(t *testing.T) { + // Start with a space and an empty reserved set. + // This test will attempt to fragment the space by pulling out + space := _p("10.128.0.0/12").Set() + available := space.NumAddresses() + + reserved := NewSet_() + + rand.Seed(29) + for available > 0 { + // This is the most we can pull out. Assuming we avoid + // fragmentation, it should be the largest power of two that is + // less than or equal to the number of available addresses. + maxExponent := log2(available) + + // Finding the maximum prefix here, proves we are avoiding fragmentation + maxPrefix, err := space.FindPrefixWithLength(reserved, 32-maxExponent) + assert.Nil(t, err) + assert.Equal(t, pow2(maxExponent), maxPrefix.NumAddresses()) + assert.Equal(t, int64(0), reserved.Intersection(maxPrefix).NumAddresses()) + + // Pull out a random sized prefix up to the maximum size to attempt to further fragment the space. + randomSize := (rand.Uint32()%maxExponent + 1) + if randomSize > 12 { + randomSize = 12 + } + + randomSizePrefix, err := space.FindPrefixWithLength(reserved, 32-randomSize) + assert.Nil(t, err) + assert.Equal(t, pow2(randomSize), randomSizePrefix.NumAddresses()) + assert.Equal(t, int64(0), reserved.Intersection(randomSizePrefix).NumAddresses()) + + // Reserve only the random sized one + reserved.Insert(randomSizePrefix) + available -= randomSizePrefix.NumAddresses() + assert.Equal(t, available, space.NumAddresses()-reserved.NumAddresses()) + } + }) +} + +func pow2(x uint32) int64 { + return int64(math.Pow(2, float64(x))) +} + +func log2(available_addresses int64) uint32 { + return uint32(math.Log2(float64(available_addresses))) } func countPrefixes(s Set) int { From 85893cd6bed255748d1ff1b735523653a3216a1d Mon Sep 17 00:00:00 2001 From: Carl Baldwin Date: Mon, 3 Jun 2024 23:15:12 -0600 Subject: [PATCH 3/5] more efficient implementation It doesn't necessarily need to complete the entire difference operation. It can look for the best prefix while it is performing the diff. --- ipv4/set.go | 3 +- ipv4/setnode.go | 107 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 92 insertions(+), 18 deletions(-) diff --git a/ipv4/set.go b/ipv4/set.go index 86e8fc7..43f06a9 100644 --- a/ipv4/set.go +++ b/ipv4/set.go @@ -346,8 +346,7 @@ func (me Set) isValid() bool { // 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) + prefix, err := me.trie.FindSmallestContainingPrefix(reserved.Set().trie, length) if err != nil { return Prefix{}, fmt.Errorf("no room for prefix of given length") } diff --git a/ipv4/setnode.go b/ipv4/setnode.go index 6681cd4..6e3c2a3 100644 --- a/ipv4/setnode.go +++ b/ipv4/setnode.go @@ -298,7 +298,31 @@ func (me *setNode) Walk(callback func(Prefix, interface{}) bool) bool { return (*trieNode)(me).Walk(callback) } -func (me *setNode) FindSmallestContainingPrefix(length uint32) (Prefix, error) { +func best(left, right func() (Prefix, error), length uint32) (Prefix, error) { + lPrefix, lErr := left() + if lErr == nil { + if lPrefix.length == length { + return lPrefix, nil + } + rPrefix, rErr := right() + if rErr == nil { + if lPrefix.length < rPrefix.length { + return rPrefix, nil + } else { + return lPrefix, nil + } + } + return lPrefix, nil + } + + rPrefix, rErr := right() + if rErr == nil { + return rPrefix, nil + } + return Prefix{}, fmt.Errorf("cannot find containing prefix") +} + +func (me *setNode) findSmallestContainingPrefix(length uint32) (Prefix, error) { if me == nil || length < me.Prefix.length { return Prefix{}, fmt.Errorf("cannot find containing prefix") } @@ -309,23 +333,74 @@ func (me *setNode) FindSmallestContainingPrefix(length uint32) (Prefix, error) { } 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 + bestPrefix, err := best( + func() (Prefix, error) { return l.findSmallestContainingPrefix(length) }, + func() (Prefix, error) { return r.findSmallestContainingPrefix(length) }, + length, + ) + if err == nil { + return bestPrefix, nil + } + if !me.isActive { + return Prefix{}, fmt.Errorf("cannot find containing prefix") + } + return me.Prefix, nil +} + +func (me *setNode) FindSmallestContainingPrefix(reserved *setNode, length uint32) (Prefix, error) { + if me == nil || length < me.Prefix.length { + return Prefix{}, fmt.Errorf("cannot find containing prefix") + } + if reserved == nil { + return me.findSmallestContainingPrefix(length) + } + + result, _, _, child := compare(me.Prefix, reserved.Prefix) + switch result { + case compareIsContained: + if reserved.isActive { + return Prefix{}, fmt.Errorf("cannot find containing prefix") } - case lErr == nil: - return lPrefix, nil - case rErr == nil: - return rPrefix, nil - default: - if !me.isActive { + return me.FindSmallestContainingPrefix((*setNode)(reserved.children[child]), length) + case compareDisjoint: + return me.findSmallestContainingPrefix(length) + } + + if !me.isActive { + return best( + func() (Prefix, error) { return me.Left().FindSmallestContainingPrefix(reserved, length) }, + func() (Prefix, error) { return me.Right().FindSmallestContainingPrefix(reserved, length) }, + length, + ) + } + + // Assumes `me` is active as checked above + halves := func() (a, b *setNode) { + aPrefix, bPrefix := me.Prefix.Halves() + return setNodeFromPrefix(aPrefix), setNodeFromPrefix(bPrefix) + } + + switch result { + case compareSame: + if reserved.isActive { return Prefix{}, fmt.Errorf("cannot find containing prefix") } - return me.Prefix, nil + left, right := halves() + return best( + func() (Prefix, error) { return left.FindSmallestContainingPrefix(reserved.Left(), length) }, + func() (Prefix, error) { return right.FindSmallestContainingPrefix(reserved.Right(), length) }, + length, + ) + + case compareContains: + left, right := halves() + halves := [2]*setNode{left, right} + whole, partial := halves[(child+1)%2], halves[child] + return best( + func() (Prefix, error) { return whole.findSmallestContainingPrefix(length) }, + func() (Prefix, error) { return partial.FindSmallestContainingPrefix(reserved, length) }, + length, + ) } + panic("unreachable") } From 5c47da69a33afcd639cb7c9ebe2ba455737f76c3 Mon Sep 17 00:00:00 2001 From: Carl Baldwin Date: Thu, 6 Jun 2024 17:49:54 -0600 Subject: [PATCH 4/5] implement FindPrefixWithLength for IPv6 --- ipv6/set.go | 16 +++ ipv6/set_test.go | 257 +++++++++++++++++++++++++++++++++++++++++++++++ ipv6/setnode.go | 111 ++++++++++++++++++++ 3 files changed, 384 insertions(+) diff --git a/ipv6/set.go b/ipv6/set.go index 5b95048..2928d9f 100644 --- a/ipv6/set.go +++ b/ipv6/set.go @@ -328,3 +328,19 @@ 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) { + prefix, err := me.trie.FindSmallestContainingPrefix(reserved.Set().trie, 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/ipv6/set_test.go b/ipv6/set_test.go index ed6848a..a720b42 100644 --- a/ipv6/set_test.go +++ b/ipv6/set_test.go @@ -1,6 +1,8 @@ package ipv6 import ( + "math" + "math/rand" "strconv" "sync" "testing" @@ -871,3 +873,258 @@ func TestSetNumPrefixesStairs(t *testing.T) { }) } } + +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("::ffff:10.0.0.0/104"), + }, + length: 120, + change: 1, + }, { + description: "find adjacent", + space: []SetI{ + _p("::ffff:10.0.0.0/104"), + }, + reserved: []SetI{ + _p("::ffff:10.224.123.0/120"), + }, + length: 120, + expected: _p("::ffff:10.224.122.0/120"), + }, { + description: "many fewer prefixes", + space: []SetI{ + _p("::ffff:10.0.0.0/112"), + }, + reserved: []SetI{ + _p("::ffff:10.0.1.0/120"), + _p("::ffff:10.0.2.0/119"), + _p("::ffff:10.0.4.0/118"), + _p("::ffff:10.0.8.0/117"), + _p("::ffff:10.0.16.0/116"), + _p("::ffff:10.0.32.0/115"), + _p("::ffff:10.0.64.0/114"), + _p("::ffff:10.0.128.0/113"), + }, + length: 120, + change: -7, + }, { + description: "toobig", + space: []SetI{ + _p("::ffff:10.0.0.0/104"), + }, + reserved: []SetI{ + _p("::ffff:10.128.0.0/105"), + _p("::ffff:10.64.0.0/106"), + _p("::ffff:10.32.0.0/107"), + _p("::ffff:10.16.0.0/108"), + }, + length: 107, + err: true, + }, { + description: "full", + space: []SetI{ + _p("::ffff:10.0.0.0/104"), + }, + length: 103, + err: true, + }, { + description: "random disjoint example", + space: []SetI{ + _p("::ffff:10.0.0.0/118"), + _p("::ffff:192.168.0.0/117"), + _p("::ffff:172.16.0.0/116"), + }, + reserved: []SetI{ + _p("::ffff:192.168.0.0/117"), + _p("::ffff:172.16.0.0/117"), + _p("::ffff:172.16.8.0/118"), + _p("::ffff:10.0.0.0/118"), + _p("::ffff:172.16.12.0/120"), + _p("::ffff:172.16.14.0/120"), + _p("::ffff:172.16.15.0/120"), + }, + length: 120, + expected: _p("::ffff:172.16.13.0/120"), + change: 1, + }, { + description: "too fragmented", + space: []SetI{ + _p("::ffff:10.0.0.0/120"), + }, + reserved: []SetI{ + _p("::ffff:10.0.0.0/123"), + _p("::ffff:10.0.0.64/123"), + _p("::ffff:10.0.0.128/123"), + _p("::ffff:10.0.0.192/123"), + }, + length: 121, + 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.True(t, reserved.Intersection(prefix).IsEmpty()) + + // 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) + }) + } + + t.Run("randomized", func(t *testing.T) { + // Start with a space and an empty reserved set. + // This test will attempt to fragment the space by pulling out + space := _p("::ffff:10.128.0.0/100").Set() + available, _ := space.NumPrefixes(128) + + reserved := NewSet_() + + rand.Seed(29) + for available > 0 { + // This is the most we can pull out. Assuming we avoid + // fragmentation, it should be the largest power of two that is + // less than or equal to the number of available addresses. + maxExponent := log2(available) + + // Finding the maximum prefix here, proves we are avoiding fragmentation + maxPrefix, err := space.FindPrefixWithLength(reserved, 128-maxExponent) + assert.Nil(t, err) + maxPrefixes, _ := maxPrefix.NumPrefixes(128) + assert.Equal(t, pow2(maxExponent), maxPrefixes) + assert.True(t, reserved.Intersection(maxPrefix).IsEmpty()) + + // Pull out a random sized prefix up to the maximum size to attempt to further fragment the space. + randomSize := (rand.Uint32()%maxExponent + 1) + if randomSize > 12 { + randomSize = 12 + } + + randomSizePrefix, err := space.FindPrefixWithLength(reserved, 128-randomSize) + assert.Nil(t, err) + randomSizePrefixes, _ := randomSizePrefix.NumPrefixes(128) + assert.Equal(t, pow2(randomSize), randomSizePrefixes) + assert.True(t, reserved.Intersection(randomSizePrefix).IsEmpty()) + + // Reserve only the random sized one + reserved.Insert(randomSizePrefix) + available -= randomSizePrefixes + spacePrefixes, _ := space.NumPrefixes(128) + reservedPrefixes, _ := reserved.Set().NumPrefixes(128) + assert.Equal(t, available, spacePrefixes-reservedPrefixes) + } + }) + + t.Run("randomized 64", func(t *testing.T) { + // Essentially the same as the previous test but hits the upper 64 of the address range + // Start with a space and an empty reserved set. + // This test will attempt to fragment the space by pulling out + space := _p("2001:db8::/48").Set() + available, _ := space.NumPrefixes(64) + + reserved := NewSet_() + + rand.Seed(17) + for available > 0 { + // This is the most we can pull out. Assuming we avoid + // fragmentation, it should be the largest power of two that is + // less than or equal to the number of available addresses. + maxExponent := log2(available) + + // Finding the maximum prefix here, proves we are avoiding fragmentation + maxPrefix, err := space.FindPrefixWithLength(reserved, 64-maxExponent) + assert.Nil(t, err) + maxPrefixes, _ := maxPrefix.NumPrefixes(64) + assert.Equal(t, pow2(maxExponent), maxPrefixes) + assert.True(t, reserved.Intersection(maxPrefix).IsEmpty()) + + // Pull out a random sized prefix up to the maximum size to attempt to further fragment the space. + randomSize := (rand.Uint32()%maxExponent + 1) + if randomSize > 12 { + randomSize = 12 + } + + randomSizePrefix, err := space.FindPrefixWithLength(reserved, 64-randomSize) + assert.Nil(t, err) + randomSizePrefixes, _ := randomSizePrefix.NumPrefixes(64) + assert.Equal(t, pow2(randomSize), randomSizePrefixes) + assert.True(t, reserved.Intersection(randomSizePrefix).IsEmpty()) + + // Reserve only the random sized one + reserved.Insert(randomSizePrefix) + available -= randomSizePrefixes + spacePrefixes, _ := space.NumPrefixes(64) + reservedPrefixes, _ := reserved.Set().NumPrefixes(64) + assert.Equal(t, available, spacePrefixes-reservedPrefixes) + } + }) +} + +func pow2(x uint32) uint64 { + return uint64(math.Pow(2, float64(x))) +} + +func log2(i uint64) uint32 { + return uint32(math.Log2(float64(i))) +} + +func countPrefixes(s Set) int { + var numPrefixes int + s.WalkPrefixes(func(_ Prefix) bool { + numPrefixes += 1 + return true + }) + return numPrefixes +} diff --git a/ipv6/setnode.go b/ipv6/setnode.go index b372da6..15d788a 100644 --- a/ipv6/setnode.go +++ b/ipv6/setnode.go @@ -1,5 +1,9 @@ package ipv6 +import ( + "fmt" +) + // setNode is currently the same data structure as trieNode. However, // its purpose is to implement a set of keys. Hence, values in the underlying // data structure are completely ignored. Aliasing it in this way allows me to @@ -291,3 +295,110 @@ func (me *setNode) height() int { func (me *setNode) Walk(callback func(Prefix, interface{}) bool) bool { return (*trieNode)(me).Walk(callback) } + +func best(left, right func() (Prefix, error), length uint32) (Prefix, error) { + lPrefix, lErr := left() + if lErr == nil { + if lPrefix.length == length { + return lPrefix, nil + } + rPrefix, rErr := right() + if rErr == nil { + if lPrefix.length < rPrefix.length { + return rPrefix, nil + } else { + return lPrefix, nil + } + } + return lPrefix, nil + } + + rPrefix, rErr := right() + if rErr == nil { + return rPrefix, nil + } + return Prefix{}, fmt.Errorf("cannot find containing prefix") +} + +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]) + bestPrefix, err := best( + func() (Prefix, error) { return l.findSmallestContainingPrefix(length) }, + func() (Prefix, error) { return r.findSmallestContainingPrefix(length) }, + length, + ) + if err == nil { + return bestPrefix, nil + } + if !me.isActive { + return Prefix{}, fmt.Errorf("cannot find containing prefix") + } + return me.Prefix, nil +} + +func (me *setNode) FindSmallestContainingPrefix(reserved *setNode, length uint32) (Prefix, error) { + if me == nil || length < me.Prefix.length { + return Prefix{}, fmt.Errorf("cannot find containing prefix") + } + if reserved == nil { + return me.findSmallestContainingPrefix(length) + } + + result, _, _, child := compare(me.Prefix, reserved.Prefix) + switch result { + case compareIsContained: + if reserved.isActive { + return Prefix{}, fmt.Errorf("cannot find containing prefix") + } + return me.FindSmallestContainingPrefix((*setNode)(reserved.children[child]), length) + case compareDisjoint: + return me.findSmallestContainingPrefix(length) + } + + if !me.isActive { + return best( + func() (Prefix, error) { return me.Left().FindSmallestContainingPrefix(reserved, length) }, + func() (Prefix, error) { return me.Right().FindSmallestContainingPrefix(reserved, length) }, + length, + ) + } + + // Assumes `me` is active as checked above + halves := func() (a, b *setNode) { + aPrefix, bPrefix := me.Prefix.Halves() + return setNodeFromPrefix(aPrefix), setNodeFromPrefix(bPrefix) + } + + switch result { + case compareSame: + if reserved.isActive { + return Prefix{}, fmt.Errorf("cannot find containing prefix") + } + left, right := halves() + return best( + func() (Prefix, error) { return left.FindSmallestContainingPrefix(reserved.Left(), length) }, + func() (Prefix, error) { return right.FindSmallestContainingPrefix(reserved.Right(), length) }, + length, + ) + + case compareContains: + left, right := halves() + halves := [2]*setNode{left, right} + whole, partial := halves[(child+1)%2], halves[child] + return best( + func() (Prefix, error) { return whole.findSmallestContainingPrefix(length) }, + func() (Prefix, error) { return partial.FindSmallestContainingPrefix(reserved, length) }, + length, + ) + } + panic("unreachable") +} From fb7755fdf992cab2b55bd250ef713b03250ad031 Mon Sep 17 00:00:00 2001 From: Carl Baldwin Date: Thu, 6 Jun 2024 18:16:47 -0600 Subject: [PATCH 5/5] rename it --- ipv4/set.go | 4 ++-- ipv4/set_test.go | 8 ++++---- ipv6/set.go | 4 ++-- ipv6/set_test.go | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ipv4/set.go b/ipv4/set.go index 43f06a9..3cd3c42 100644 --- a/ipv4/set.go +++ b/ipv4/set.go @@ -341,11 +341,11 @@ func (me Set) isValid() bool { return me.trie.isValid() } -// FindPrefixWithLength returns a Prefix with a Mask of the given prefix length +// FindAvailablePrefix 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) { +func (me Set) FindAvailablePrefix(reserved SetI, length uint32) (Prefix, error) { prefix, err := me.trie.FindSmallestContainingPrefix(reserved.Set().trie, length) if err != nil { return Prefix{}, fmt.Errorf("no room for prefix of given length") diff --git a/ipv4/set_test.go b/ipv4/set_test.go index 6356562..81630da 100644 --- a/ipv4/set_test.go +++ b/ipv4/set_test.go @@ -955,7 +955,7 @@ func TestSetNumPrefixesStairs(t *testing.T) { } } -func TestFindPrefixWithLength(t *testing.T) { +func TestFindAvailablePrefix(t *testing.T) { tests := []struct { description string space []SetI @@ -1072,7 +1072,7 @@ func TestFindPrefixWithLength(t *testing.T) { }) // Call the method under test to find the best allocation to avoid fragmentation. - prefix, err := space.FindPrefixWithLength(reserved, tt.length) + prefix, err := space.FindAvailablePrefix(reserved, tt.length) assert.Equal(t, tt.err, err != nil) if err != nil { @@ -1121,7 +1121,7 @@ func TestFindPrefixWithLength(t *testing.T) { maxExponent := log2(available) // Finding the maximum prefix here, proves we are avoiding fragmentation - maxPrefix, err := space.FindPrefixWithLength(reserved, 32-maxExponent) + maxPrefix, err := space.FindAvailablePrefix(reserved, 32-maxExponent) assert.Nil(t, err) assert.Equal(t, pow2(maxExponent), maxPrefix.NumAddresses()) assert.Equal(t, int64(0), reserved.Intersection(maxPrefix).NumAddresses()) @@ -1132,7 +1132,7 @@ func TestFindPrefixWithLength(t *testing.T) { randomSize = 12 } - randomSizePrefix, err := space.FindPrefixWithLength(reserved, 32-randomSize) + randomSizePrefix, err := space.FindAvailablePrefix(reserved, 32-randomSize) assert.Nil(t, err) assert.Equal(t, pow2(randomSize), randomSizePrefix.NumAddresses()) assert.Equal(t, int64(0), reserved.Intersection(randomSizePrefix).NumAddresses()) diff --git a/ipv6/set.go b/ipv6/set.go index 2928d9f..ae95cb3 100644 --- a/ipv6/set.go +++ b/ipv6/set.go @@ -329,11 +329,11 @@ func (me Set) isValid() bool { return me.trie.isValid() } -// FindPrefixWithLength returns a Prefix with a Mask of the given prefix length +// FindAvailablePrefix 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) { +func (me Set) FindAvailablePrefix(reserved SetI, length uint32) (Prefix, error) { prefix, err := me.trie.FindSmallestContainingPrefix(reserved.Set().trie, length) if err != nil { return Prefix{}, fmt.Errorf("no room for prefix of given length") diff --git a/ipv6/set_test.go b/ipv6/set_test.go index a720b42..ce903dc 100644 --- a/ipv6/set_test.go +++ b/ipv6/set_test.go @@ -874,7 +874,7 @@ func TestSetNumPrefixesStairs(t *testing.T) { } } -func TestFindPrefixWithLength(t *testing.T) { +func TestFindAvailablePrefix(t *testing.T) { tests := []struct { description string space []SetI @@ -991,7 +991,7 @@ func TestFindPrefixWithLength(t *testing.T) { }) // Call the method under test to find the best allocation to avoid fragmentation. - prefix, err := space.FindPrefixWithLength(reserved, tt.length) + prefix, err := space.FindAvailablePrefix(reserved, tt.length) assert.Equal(t, tt.err, err != nil) if err != nil { @@ -1040,7 +1040,7 @@ func TestFindPrefixWithLength(t *testing.T) { maxExponent := log2(available) // Finding the maximum prefix here, proves we are avoiding fragmentation - maxPrefix, err := space.FindPrefixWithLength(reserved, 128-maxExponent) + maxPrefix, err := space.FindAvailablePrefix(reserved, 128-maxExponent) assert.Nil(t, err) maxPrefixes, _ := maxPrefix.NumPrefixes(128) assert.Equal(t, pow2(maxExponent), maxPrefixes) @@ -1052,7 +1052,7 @@ func TestFindPrefixWithLength(t *testing.T) { randomSize = 12 } - randomSizePrefix, err := space.FindPrefixWithLength(reserved, 128-randomSize) + randomSizePrefix, err := space.FindAvailablePrefix(reserved, 128-randomSize) assert.Nil(t, err) randomSizePrefixes, _ := randomSizePrefix.NumPrefixes(128) assert.Equal(t, pow2(randomSize), randomSizePrefixes) @@ -1084,7 +1084,7 @@ func TestFindPrefixWithLength(t *testing.T) { maxExponent := log2(available) // Finding the maximum prefix here, proves we are avoiding fragmentation - maxPrefix, err := space.FindPrefixWithLength(reserved, 64-maxExponent) + maxPrefix, err := space.FindAvailablePrefix(reserved, 64-maxExponent) assert.Nil(t, err) maxPrefixes, _ := maxPrefix.NumPrefixes(64) assert.Equal(t, pow2(maxExponent), maxPrefixes) @@ -1096,7 +1096,7 @@ func TestFindPrefixWithLength(t *testing.T) { randomSize = 12 } - randomSizePrefix, err := space.FindPrefixWithLength(reserved, 64-randomSize) + randomSizePrefix, err := space.FindAvailablePrefix(reserved, 64-randomSize) assert.Nil(t, err) randomSizePrefixes, _ := randomSizePrefix.NumPrefixes(64) assert.Equal(t, pow2(randomSize), randomSizePrefixes)