浏览代码

plugins: IPv6 Prefix Delegation allocator scaffolding

To build an IPv6 Prefix Delegation plugin, we'll need allocation
strategies as well as the plugin itself.
This adds scaffolding for prefix allocators

 * allocator.go contains interfaces to interact with the allocators
 * ipcalc has helper functions to simplify indexing tables/hash ipv6
   addresses
 * fixedsize is a trivial allocator that always reserves a prefix of the
   same size (eg /64) regardless of what the client requests

Signed-off-by: Anatole Denis <anatole@unverle.fr>
Anatole Denis 6 年之前
父节点
当前提交
6ee08879be

+ 1 - 0
go.mod

@@ -16,6 +16,7 @@ require (
 	github.com/spf13/cast v1.3.1
 	github.com/spf13/viper v1.7.0
 	github.com/u-root/u-root v6.0.0+incompatible // indirect
+	github.com/willf/bitset v1.1.10
 	github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
 	golang.org/x/net v0.0.0-20200625001655-4c5254603344
 )

+ 2 - 0
go.sum

@@ -219,6 +219,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/u-root/u-root v6.0.0+incompatible h1:YqPGmRoRyYmeg17KIWFRSyVq6LX5T6GSzawyA6wG6EE=
 github.com/u-root/u-root v6.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY=
+github.com/willf/bitset v1.1.10 h1:NotGKqX0KwQ72NUzqrjZq5ipPNDQex9lo3WpaS8L2sc=
+github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
 github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
 github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=

+ 44 - 0
plugins/prefix/allocators/allocator.go

@@ -0,0 +1,44 @@
+// Copyright 2018-present the CoreDHCP Authors. All rights reserved
+// This source code is licensed under the MIT license found in the
+// LICENSE file in the root directory of this source tree.
+
+// Package allocators provides the interface and the algorithm(s) for allocation of ipv6
+// prefixes of various sizes within a larger prefix.
+// There are many many parallels with memory allocation.
+package allocators
+
+import (
+	"fmt"
+	"net"
+)
+
+// Allocator is the interface to the address allocator. It only finds and
+// allocates blocks and is not concerned with lease-specific questions like
+// expiration (ie garbage collection needs to be handled separately)
+type Allocator interface {
+	// Allocate finds a suitable prefix of the given size and returns it.
+	//
+	// hint is a prefix, which the client desires especially, and that the
+	// allocator MAY try to return; the allocator SHOULD try to return a prefix of
+	// the same size as the given hint prefix. The allocator MUST NOT return an
+	// error if a prefix was successfully assigned, even if the prefix has nothing
+	// in common with the hinted prefix
+	Allocate(hint net.IPNet) (net.IPNet, error)
+
+	// Free returns the prefix containing the given network to the pool
+	//
+	// Free may return a DoubleFreeError if the prefix being returned was not
+	// previously allocated
+	Free(net.IPNet) error
+}
+
+// ErrDoubleFree is an error type returned by Allocator.Free() when a
+// non-allocated block is passed
+type ErrDoubleFree struct {
+	Loc net.IPNet
+}
+
+// String returns a human-readable error message for a DoubleFree error
+func (err *ErrDoubleFree) Error() string {
+	return fmt.Sprint("Attempted to free unallocated block at ", err.Loc.String())
+}

+ 131 - 0
plugins/prefix/allocators/fixedsize/fixedsize.go

@@ -0,0 +1,131 @@
+// Copyright 2018-present the CoreDHCP Authors. All rights reserved
+// This source code is licensed under the MIT license found in the
+// LICENSE file in the root directory of this source tree.
+
+// This allocator only returns prefixes of a single size
+// This is much simpler to implement (reduces the problem to an equivalent of
+// single ip allocations), probably makes sense in cases where the available
+// range is much larger than the expected number of clients. Also is what KEA
+// does so at least it's not worse than that
+
+package fixedsize
+
+import (
+	"errors"
+	"fmt"
+	"net"
+	"strconv"
+
+	"github.com/willf/bitset"
+
+	"github.com/coredhcp/coredhcp/logger"
+	"github.com/coredhcp/coredhcp/plugins/prefix/allocators"
+)
+
+var log = logger.GetLogger("plugins/prefix/allocator")
+
+// Allocator is a prefix allocator allocating in chunks of a fixed size
+// regardless of the size requested by the client.
+// It consumes an amount of memory proportional to the total amount of available prefixes
+type Allocator struct {
+	containing net.IPNet
+	page       int
+	bitmap     *bitset.BitSet
+}
+
+type block struct {
+}
+
+// prefix must verify: containing.Mask.Size < prefix.Mask.Size < page
+func (a *Allocator) toIndex(base net.IP) (uint, error) {
+	value, err := allocators.Offset(base, a.containing.IP, a.page)
+	if err != nil {
+		return 0, fmt.Errorf("Cannot compute prefix index: %w", err)
+	}
+
+	return uint(value), nil
+}
+
+func (a *Allocator) toPrefix(idx uint) (net.IP, error) {
+	return allocators.AddPrefixes(a.containing.IP, uint64(idx), uint64(a.page))
+}
+
+// Allocate reserves a maxsize-sized block and returns a block of size
+// min(maxsize, hint.size)
+func (a *Allocator) Allocate(hint net.IPNet) (ret net.IPNet, err error) {
+
+	// Ensure size is max(maxsize, hint.size)
+	reqSize, hintErr := hint.Mask.Size()
+	if reqSize < a.page || hintErr != 128 {
+		reqSize = a.page
+	}
+	ret.Mask = net.CIDRMask(reqSize, 128)
+
+	// Try to allocate the requested prefix
+	if hint.IP.To16() != nil && a.containing.Contains(hint.IP) {
+		idx, hintErr := a.toIndex(hint.IP)
+		if hintErr == nil && !a.bitmap.Test(idx) {
+			a.bitmap.Set(idx)
+			ret.IP, err = a.toPrefix(idx)
+			return
+		}
+	}
+
+	// Find a free prefix
+	next, ok := a.bitmap.NextClear(0)
+	if !ok {
+		err = errors.New("No prefix available")
+		return
+	}
+	a.bitmap.Set(next)
+	ret.IP, err = a.toPrefix(next)
+	if err != nil {
+		// This violates the assumption that every index in the bitmap maps back to a valid prefix
+		err = fmt.Errorf("BUG: could not get prefix from allocation: %w", err)
+		a.bitmap.Clear(next)
+	}
+	return
+}
+
+// Free returns the given prefix to the available pool if it was taken.
+func (a *Allocator) Free(prefix net.IPNet) error {
+	idx, err := a.toIndex(prefix.IP.Mask(prefix.Mask))
+	if err != nil {
+		return fmt.Errorf("Could not find prefix in pool: %w", err)
+	}
+
+	if !a.bitmap.Test(idx) {
+		return &allocators.ErrDoubleFree{Loc: prefix}
+	}
+	a.bitmap.Clear(idx)
+	return nil
+}
+
+// NewFixedSizeAllocator creates a new allocator, allocating /`size` prefixes
+// carved out of the given `pool` prefix
+func NewFixedSizeAllocator(pool net.IPNet, size int) (*Allocator, error) {
+
+	poolSize, _ := pool.Mask.Size()
+	allocOrder := size - poolSize
+
+	if allocOrder < 0 {
+		return nil, errors.New("The size of allocated prefixes cannot be larger than the pool they're allocated from")
+	} else if allocOrder >= strconv.IntSize {
+		return nil, fmt.Errorf("A pool with more than 2^%d items is not representable", size-poolSize)
+	} else if allocOrder >= 32 {
+		log.Warningln("Using a pool of more than 2^32 elements may result in large memory consumption")
+	}
+
+	if !(1<<uint(allocOrder) <= bitset.Cap()) {
+		return nil, errors.New("Can't fit this pool using the fixedsize allocator")
+	}
+
+	alloc := Allocator{
+		containing: pool,
+		page:       size,
+
+		bitmap: bitset.New(1 << uint(allocOrder)),
+	}
+
+	return &alloc, nil
+}

+ 86 - 0
plugins/prefix/allocators/fixedsize/fixedsize_test.go

@@ -0,0 +1,86 @@
+// Copyright 2018-present the CoreDHCP Authors. All rights reserved
+// This source code is licensed under the MIT license found in the
+// LICENSE file in the root directory of this source tree.
+
+package fixedsize
+
+import (
+	"net"
+	"testing"
+)
+
+func getAllocator() *Allocator {
+	_, prefix, err := net.ParseCIDR("2001:db8::/56")
+	if err != nil {
+		panic(err)
+	}
+	alloc, err := NewFixedSizeAllocator(*prefix, 64)
+	if err != nil {
+		panic(err)
+	}
+
+	return alloc
+}
+func TestAlloc(t *testing.T) {
+	alloc := getAllocator()
+
+	net, err := alloc.Allocate(net.IPNet{})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = alloc.Free(net)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	err = alloc.Free(net)
+	if err == nil {
+		t.Fatal("Expected DoubleFree error")
+	}
+}
+
+func TestExhaust(t *testing.T) {
+	_, prefix, _ := net.ParseCIDR("2001:db8::/62")
+	alloc, _ := NewFixedSizeAllocator(*prefix, 64)
+
+	allocd := []net.IPNet{}
+	for i := 0; i < 4; i++ {
+		net, err := alloc.Allocate(net.IPNet{Mask: net.CIDRMask(64, 128)})
+		if err != nil {
+			t.Fatalf("Error before exhaustion: %v", err)
+		}
+		allocd = append(allocd, net)
+	}
+
+	_, err := alloc.Allocate(net.IPNet{})
+	if err == nil {
+		t.Fatalf("Successfully allocated more prefixes than there are in the pool")
+	}
+
+	alloc.Free(allocd[1])
+	net, err := alloc.Allocate(allocd[1])
+	if err != nil {
+		t.Fatalf("Could not reallocate after free: %v", err)
+	}
+	if !net.IP.Equal(allocd[1].IP) || net.Mask.String() != allocd[1].Mask.String() {
+		t.Fatalf("Did not obtain the right network after free: got %v, expected %v", net, allocd[1])
+	}
+
+}
+
+func TestOutOfPool(t *testing.T) {
+	alloc := getAllocator()
+	_, prefix, _ := net.ParseCIDR("fe80:abcd::/48")
+
+	res, err := alloc.Allocate(*prefix)
+	if err != nil {
+		t.Fatalf("Failed to allocate with invalid hint: %v", err)
+	}
+	if !alloc.containing.Contains(res.IP) {
+		t.Fatal("Obtained prefix outside of range: ", res)
+	}
+	if prefLen, totalLen := res.Mask.Size(); prefLen != 64 || totalLen != 128 {
+		t.Fatalf("Prefixes have wrong size %d/%d", prefLen, totalLen)
+	}
+}

+ 120 - 0
plugins/prefix/allocators/ipcalc.go

@@ -0,0 +1,120 @@
+// Copyright 2018-present the CoreDHCP Authors. All rights reserved
+// This source code is licensed under the MIT license found in the
+// LICENSE file in the root directory of this source tree.
+
+// Provides functions to add/subtract ipv6 addresses, for use in offset
+// calculations in allocators
+
+package allocators
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"math/bits"
+	"net"
+)
+
+// ErrOverflow is returned when arithmetic operations on IPs carry bits
+// over/under the 0th or 128th bit respectively
+var ErrOverflow = errors.New("Operation overflows")
+
+// Offset returns the absolute distance between addresses `a` and `b` in units
+// of /`prefixLength` subnets.
+// Both addresses will have a /`prefixLength` mask applied to them, any
+// differences of less than that will be discarded
+// If the distance is larger than 2^64 units of /`prefixLength` an error is returned
+//
+// This function is used in allocators to index bitmaps by an offset from the
+// first ip of the range
+func Offset(a, b net.IP, prefixLength int) (uint64, error) {
+	if prefixLength > 128 || prefixLength < 0 {
+		return 0, errors.New("prefix out of range")
+	}
+
+	reverse := bytes.Compare(a, b)
+	if reverse == 0 {
+		return 0, nil
+	} else if reverse < 0 {
+		a, b = b, a
+	}
+
+	// take an example of [a:b:c:d:e:f:g:h] [1:2:3:4:5:6:7:8]
+	// Cut the addresses as such: [a:b:c:d|e:f:g:h] [1:2:3:4|5:6:7:8] so we can use
+	// native integers for computation
+	ah, bh := binary.BigEndian.Uint64(a[:8]), binary.BigEndian.Uint64(b[:8])
+
+	if prefixLength <= 64 {
+		// [(a:b:c):d|e:f:g:h] - [(1:2:3):4|5:6:7:8]
+		// Only the high bits matter, so the distance always fits within 64 bits.
+		// We shift to remove anything to the right of the cut
+		// [(a:b:c):d] => [0:a:b:c]
+		return (ah - bh) >> (64 - uint(prefixLength)), nil
+	}
+
+	// General case where both high and low bits matter
+	al, bl := binary.BigEndian.Uint64(a[8:]), binary.BigEndian.Uint64(b[8:])
+	distanceLow, borrow := bits.Sub64(al, bl, 0)
+
+	// This is the distance between the high bits. depending on the prefix unit, we
+	// will shift this distance left or right
+	distanceHigh, _ := bits.Sub64(ah, bh, borrow) // [a:b:c:d] - [1:2:3:4]
+
+	// [a:b:c:(d|e:f:g):h] - [1:2:3:(4|5:6:7):8]
+	// we cut in the low bits (eg. between the parentheses)
+	// To ensure we stay within 64 bits, we need to ensure [a:b:c:d] - [1:2:3:4] = [0:0:0:d-4]
+	// so that we don't overflow when adding to the low bits
+	if distanceHigh >= (1 << (128 - uint(prefixLength))) {
+		return 0, ErrOverflow
+	}
+
+	// Schema of the carry and shifts:
+	// [a:b:c:(d]
+	//          [e:f:g):h]
+	// <--------------->   prefixLen
+	//                 <-> 128 - prefixLen (cut right)
+	// <----->             prefixLen - 64 (cut left)
+	//
+	// [a:b:c:(d] => [d:0:0:0]
+	distanceHigh <<= uint(prefixLength) - 64
+	// [e:f:g):h] => [0:e:f:g]
+	distanceLow >>= 128 - uint(prefixLength)
+	// [d:0:0:0] + [0:e:f:g] = (d:e:f:g)
+	return distanceHigh + distanceLow, nil
+}
+
+// AddPrefixes returns the `n`th /`unit` subnet after the `ip` base subnet. It
+// is the converse operation of Offset(), used to retrieve a prefix from the
+// index within the allocator table
+func AddPrefixes(ip net.IP, n, unit uint64) (net.IP, error) {
+	if unit == 0 && n != 0 {
+		return net.IP{}, ErrOverflow
+	} else if n == 0 {
+		return ip, nil
+	}
+
+	// Compute as pairs of uint64 for easier operations
+	iph, ipl := binary.BigEndian.Uint64(ip[:8]), binary.BigEndian.Uint64(ip[8:])
+
+	// Compute `n` /`unit` subnets as uint64 pair
+	var offh, offl uint64
+	if unit <= 64 {
+		offh = n << (64 - unit)
+	} else {
+		offh, offl = bits.Mul64(n, 1<<(128-unit))
+	}
+
+	// Now add the 2, check for overflow
+	ipl, carry := bits.Add64(offl, ipl, 0)
+	iph, carry = bits.Add64(offh, iph, carry)
+	if carry != 0 {
+		return net.IP{}, ErrOverflow
+	}
+
+	// Finally convert back to net.IP
+	ret := make(net.IP, net.IPv6len)
+	binary.BigEndian.PutUint64(ret[:8], iph)
+	binary.BigEndian.PutUint64(ret[8:], ipl)
+
+	return ret, nil
+}

+ 48 - 0
plugins/prefix/allocators/ipcalc_test.go

@@ -0,0 +1,48 @@
+// Copyright 2018-present the CoreDHCP Authors. All rights reserved
+// This source code is licensed under the MIT license found in the
+// LICENSE file in the root directory of this source tree.
+
+package allocators
+
+import (
+	"fmt"
+	"net"
+)
+
+func ExampleOffset() {
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 0))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 16))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 32))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 48))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 64))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 73))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 80))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 96))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 112))
+	fmt.Println(Offset(net.ParseIP("2001:db8:0:aabb::"), net.ParseIP("2001:db8:ff::34"), 128))
+	// Output:
+	// 0 <nil>
+	// 0 <nil>
+	// 0 <nil>
+	// 254 <nil>
+	// 16667973 <nil>
+	// 8534002176 <nil>
+	// 1092352278528 <nil>
+	// 71588398925611008 <nil>
+	// 0 Operation overflows
+	// 0 Operation overflows
+}
+
+func ExampleAddPrefixes() {
+	fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 64))
+	fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0x1, 128))
+	fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 32))
+	fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0x1, 16))
+	fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 65))
+	// Output:
+	// 2001:db8:0:ff:: <nil>
+	// 2001:db8::1 <nil>
+	// 2001:eb7:: <nil>
+	// 2002:db8:: <nil>
+	// 2001:db8:0:7f:8000:: <nil>
+}