add all files from Hong
This commit is contained in:
48
plugins/allocators/allocator.go
Normal file
48
plugins/allocators/allocator.go
Normal file
@@ -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 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 (
|
||||
"errors"
|
||||
"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())
|
||||
}
|
||||
|
||||
// ErrNoAddrAvail is returned when we can't allocate an IP because there's no unallocated space left
|
||||
var ErrNoAddrAvail = errors.New("no address available to allocate")
|
||||
135
plugins/allocators/bitmap/bitmap.go
Normal file
135
plugins/allocators/bitmap/bitmap.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// 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 bitmap
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins/allocators"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/allocators/bitmap")
|
||||
|
||||
// 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
|
||||
l sync.Mutex
|
||||
}
|
||||
|
||||
// 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
|
||||
a.l.Lock()
|
||||
defer a.l.Unlock()
|
||||
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 = allocators.ErrNoAddrAvail
|
||||
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)
|
||||
}
|
||||
|
||||
a.l.Lock()
|
||||
defer a.l.Unlock()
|
||||
|
||||
if !a.bitmap.Test(idx) {
|
||||
return &allocators.ErrDoubleFree{Loc: prefix}
|
||||
}
|
||||
a.bitmap.Clear(idx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewBitmapAllocator creates a new allocator, allocating /`size` prefixes
|
||||
// carved out of the given `pool` prefix
|
||||
func NewBitmapAllocator(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 bitmap allocator")
|
||||
}
|
||||
|
||||
alloc := Allocator{
|
||||
containing: pool,
|
||||
page: size,
|
||||
|
||||
bitmap: bitset.New(1 << uint(allocOrder)),
|
||||
}
|
||||
|
||||
return &alloc, nil
|
||||
}
|
||||
122
plugins/allocators/bitmap/bitmap_ipv4.go
Normal file
122
plugins/allocators/bitmap/bitmap_ipv4.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// 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 bitmap
|
||||
|
||||
// This allocator handles IPv4 assignments with a similar logic to the base bitmap, but a simpler
|
||||
// implementation due to the ability to just use uint32 for IPv4 addresses
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
"github.com/coredhcp/coredhcp/plugins/allocators"
|
||||
)
|
||||
|
||||
var (
|
||||
errNotInRange = errors.New("IPv4 address outside of allowed range")
|
||||
errInvalidIP = errors.New("invalid IPv4 address passed as input")
|
||||
)
|
||||
|
||||
// IPv4Allocator allocates IPv4 addresses, tracking utilization with a bitmap
|
||||
type IPv4Allocator struct {
|
||||
start uint32
|
||||
end uint32
|
||||
|
||||
// This bitset implementation isn't goroutine-safe, we protect it with a mutex for now
|
||||
// until we can swap for another concurrent implementation
|
||||
bitmap *bitset.BitSet
|
||||
l sync.Mutex
|
||||
}
|
||||
|
||||
func (a *IPv4Allocator) toIP(offset uint32) net.IP {
|
||||
if offset > a.end-a.start {
|
||||
panic("BUG: offset out of bounds")
|
||||
}
|
||||
|
||||
r := make(net.IP, net.IPv4len)
|
||||
binary.BigEndian.PutUint32(r, a.start+offset)
|
||||
return r
|
||||
}
|
||||
|
||||
func (a *IPv4Allocator) toOffset(ip net.IP) (uint, error) {
|
||||
if ip.To4() == nil {
|
||||
return 0, errInvalidIP
|
||||
}
|
||||
|
||||
intIP := binary.BigEndian.Uint32(ip.To4())
|
||||
if intIP < a.start || intIP > a.end {
|
||||
return 0, errNotInRange
|
||||
}
|
||||
|
||||
return uint(intIP - a.start), nil
|
||||
}
|
||||
|
||||
// Allocate reserves an IP for a client
|
||||
func (a *IPv4Allocator) Allocate(hint net.IPNet) (n net.IPNet, err error) {
|
||||
n.Mask = net.CIDRMask(32, 32)
|
||||
|
||||
// This is just a hint, ignore any error with it
|
||||
hintOffset, _ := a.toOffset(hint.IP)
|
||||
|
||||
a.l.Lock()
|
||||
defer a.l.Unlock()
|
||||
|
||||
var next uint
|
||||
// First try the exact match
|
||||
if !a.bitmap.Test(hintOffset) {
|
||||
next = hintOffset
|
||||
} else {
|
||||
// Then any available address
|
||||
avail, ok := a.bitmap.NextClear(0)
|
||||
if !ok {
|
||||
return n, allocators.ErrNoAddrAvail
|
||||
}
|
||||
next = avail
|
||||
}
|
||||
|
||||
a.bitmap.Set(next)
|
||||
n.IP = a.toIP(uint32(next))
|
||||
return
|
||||
}
|
||||
|
||||
// Free releases the given IP
|
||||
func (a *IPv4Allocator) Free(n net.IPNet) error {
|
||||
offset, err := a.toOffset(n.IP)
|
||||
if err != nil {
|
||||
return errNotInRange
|
||||
}
|
||||
|
||||
a.l.Lock()
|
||||
defer a.l.Unlock()
|
||||
|
||||
if !a.bitmap.Test(uint(offset)) {
|
||||
return &allocators.ErrDoubleFree{Loc: n}
|
||||
}
|
||||
a.bitmap.Clear(offset)
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewIPv4Allocator creates a new allocator suitable for giving out IPv4 addresses
|
||||
func NewIPv4Allocator(start, end net.IP) (*IPv4Allocator, error) {
|
||||
if start.To4() == nil || end.To4() == nil {
|
||||
return nil, fmt.Errorf("invalid IPv4 addresses given to create the allocator: [%s,%s]", start, end)
|
||||
}
|
||||
|
||||
alloc := IPv4Allocator{
|
||||
start: binary.BigEndian.Uint32(start.To4()),
|
||||
end: binary.BigEndian.Uint32(end.To4()),
|
||||
}
|
||||
|
||||
if alloc.start > alloc.end {
|
||||
return nil, errors.New("no IPs in the given range to allocate")
|
||||
}
|
||||
alloc.bitmap = bitset.New(uint(alloc.end - alloc.start + 1))
|
||||
|
||||
return &alloc, nil
|
||||
}
|
||||
63
plugins/allocators/bitmap/bitmap_ipv4_test.go
Normal file
63
plugins/allocators/bitmap/bitmap_ipv4_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// 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 bitmap
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getv4Allocator() *IPv4Allocator {
|
||||
alloc, err := NewIPv4Allocator(net.IPv4(192, 0, 2, 0), net.IPv4(192, 0, 2, 255))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return alloc
|
||||
}
|
||||
func Test4Alloc(t *testing.T) {
|
||||
alloc := getv4Allocator()
|
||||
|
||||
net1, err := alloc.Allocate(net.IPNet{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
net2, err := alloc.Allocate(net.IPNet{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if net1.IP.Equal(net2.IP) {
|
||||
t.Fatal("That address was already allocated")
|
||||
}
|
||||
|
||||
err = alloc.Free(net1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = alloc.Free(net1)
|
||||
if err == nil {
|
||||
t.Fatal("Expected DoubleFree error")
|
||||
}
|
||||
}
|
||||
|
||||
func Test4OutOfPool(t *testing.T) {
|
||||
alloc := getv4Allocator()
|
||||
|
||||
hint := net.IPv4(198, 51, 100, 5)
|
||||
res, err := alloc.Allocate(net.IPNet{IP: hint, Mask: net.CIDRMask(32, 32)})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate with invalid hint: %v", err)
|
||||
}
|
||||
_, prefix, _ := net.ParseCIDR("192.0.2.0/24")
|
||||
if !prefix.Contains(res.IP) {
|
||||
t.Fatal("Obtained prefix outside of range: ", res)
|
||||
}
|
||||
if prefLen, totalLen := res.Mask.Size(); prefLen != 32 || totalLen != 32 {
|
||||
t.Fatalf("Prefixes have wrong size %d/%d", prefLen, totalLen)
|
||||
}
|
||||
}
|
||||
137
plugins/allocators/bitmap/bitmap_test.go
Normal file
137
plugins/allocators/bitmap/bitmap_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// 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 bitmap
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/rand"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
)
|
||||
|
||||
func getAllocator(bits int) *Allocator {
|
||||
_, prefix, err := net.ParseCIDR("2001:db8::/56")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
alloc, err := NewBitmapAllocator(*prefix, 56+bits)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return alloc
|
||||
}
|
||||
func TestAlloc(t *testing.T) {
|
||||
alloc := getAllocator(8)
|
||||
|
||||
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, _ := NewBitmapAllocator(*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")
|
||||
}
|
||||
|
||||
err = alloc.Free(allocd[1])
|
||||
if err != nil {
|
||||
t.Fatalf("Could not free: %v", err)
|
||||
}
|
||||
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(8)
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
|
||||
func prefixSizeForAllocs(allocs int) int {
|
||||
return int(math.Ceil(math.Log2(float64(allocs))))
|
||||
}
|
||||
|
||||
// Benchmark parallel Allocate, when the bitmap is mostly empty and we're allocating few values
|
||||
// compared to the available allocations
|
||||
func BenchmarkParallelAllocInitiallyEmpty(b *testing.B) {
|
||||
// Run with -race to debug concurrency issues
|
||||
|
||||
alloc := getAllocator(prefixSizeForAllocs(b.N) + 2) // Use max 25% of the bitmap (initially empty)
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if net, err := alloc.Allocate(net.IPNet{}); err != nil {
|
||||
b.Logf("Could not allocate (got %v and an error): %v", net, err)
|
||||
b.Fail()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkParallelAllocPartiallyFilled(b *testing.B) {
|
||||
// We'll make a bitmap with 2x the number of allocs we want to make.
|
||||
// Then randomly fill it to about 50% utilization
|
||||
alloc := getAllocator(prefixSizeForAllocs(b.N) + 1)
|
||||
|
||||
// Build a replacement bitmap that we'll put in the allocator, with approx. 50% of values filled
|
||||
newbmap := make([]uint64, alloc.bitmap.Len())
|
||||
for i := uint(0); i < alloc.bitmap.Len(); i++ {
|
||||
newbmap[i] = rand.Uint64()
|
||||
}
|
||||
alloc.bitmap = bitset.From(newbmap)
|
||||
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
if net, err := alloc.Allocate(net.IPNet{}); err != nil {
|
||||
b.Logf("Could not allocate (got %v and an error): %v", net, err)
|
||||
b.Fail()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
126
plugins/allocators/ipcalc.go
Normal file
126
plugins/allocators/ipcalc.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// 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
|
||||
}
|
||||
if len(ip) != 16 {
|
||||
// We don't actually care if they're true v6 or v4-mapped,
|
||||
// but they need to be 128-bit to handle as 64-bit ints
|
||||
return net.IP{}, errors.New("AddPrefixes needs 128-bit IPs")
|
||||
}
|
||||
|
||||
// Compute as pairs of uint64 for easier operations
|
||||
// This could all be 1 function call if go had 128-bit integers
|
||||
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
|
||||
}
|
||||
77
plugins/allocators/ipcalc_test.go
Normal file
77
plugins/allocators/ipcalc_test.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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"
|
||||
"testing"
|
||||
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
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))
|
||||
// Error cases
|
||||
fmt.Println(AddPrefixes(net.ParseIP("2001:db8::"), 0xff, 8))
|
||||
fmt.Println(AddPrefixes(net.IP{10, 0, 0, 1}, 64, 32))
|
||||
// Output:
|
||||
// 2001:db8:0:ff:: <nil>
|
||||
// 2001:db8::1 <nil>
|
||||
// 2001:eb7:: <nil>
|
||||
// 2002:db8:: <nil>
|
||||
// 2001:db8:0:7f:8000:: <nil>
|
||||
// <nil> Operation overflows
|
||||
// <nil> AddPrefixes needs 128-bit IPs
|
||||
}
|
||||
|
||||
// Offset is used as a hash function, so it needs to be reasonably fast
|
||||
func BenchmarkOffset(b *testing.B) {
|
||||
// Need predictable randomness for benchmark reproducibility
|
||||
rng := rand.New(rand.NewSource(0))
|
||||
addresses := make([]byte, b.N*net.IPv6len*2)
|
||||
_, err := rng.Read(addresses)
|
||||
if err != nil {
|
||||
b.Fatalf("Could not generate random addresses: %v", err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// The arrays will be in cache, so this should amortize to measure mostly just the offset
|
||||
// computation itself
|
||||
_, _ = Offset(
|
||||
addresses[i*2*net.IPv6len:(i*2+1)*net.IPv6len],
|
||||
addresses[(i*2+1)*net.IPv6len:(i+1)*2*net.IPv6len],
|
||||
(i*4)%128,
|
||||
)
|
||||
}
|
||||
}
|
||||
86
plugins/autoconfigure/plugin.go
Normal file
86
plugins/autoconfigure/plugin.go
Normal file
@@ -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 autoconfigure
|
||||
|
||||
// This plugin implements RFC2563:
|
||||
// 1. If the client has been allocated an IP address, do nothing
|
||||
// 2. If the client has not been allocated an IP address
|
||||
// (yiaddr=0.0.0.0), then:
|
||||
// 2a. If the client has requested the "AutoConfigure" option,
|
||||
// then add the defined value to the response
|
||||
// 2b. Otherwise, terminate processing and send no reply
|
||||
//
|
||||
// This plugin should be used at the end of the plugin chain,
|
||||
// after any IP address allocation has taken place.
|
||||
//
|
||||
// The optional argument is the string "DoNotAutoConfigure" or
|
||||
// "AutoConfigure" (or "0" or "1" respectively). The default
|
||||
// is DoNotAutoConfigure.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/autoconfigure")
|
||||
|
||||
var autoconfigure dhcpv4.AutoConfiguration
|
||||
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "autoconfigure",
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
var argMap = map[string]dhcpv4.AutoConfiguration{
|
||||
"0": dhcpv4.AutoConfiguration(0),
|
||||
"1": dhcpv4.AutoConfiguration(1),
|
||||
"DoNotAutoConfigure": dhcpv4.DoNotAutoConfigure,
|
||||
"AutoConfigure": dhcpv4.AutoConfigure,
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
if len(args) > 0 {
|
||||
var ok bool
|
||||
autoconfigure, ok = argMap[args[0]]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected value '%v' for autoconfigure argument", args[0])
|
||||
}
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return nil, errors.New("too many arguments")
|
||||
}
|
||||
return Handler4, nil
|
||||
}
|
||||
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
if resp.MessageType() != dhcpv4.MessageTypeOffer || !resp.YourIPAddr.IsUnspecified() {
|
||||
return resp, false
|
||||
}
|
||||
|
||||
ac, ok := req.AutoConfigure()
|
||||
if ok {
|
||||
resp.UpdateOption(dhcpv4.OptAutoConfigure(autoconfigure))
|
||||
log.WithFields(logrus.Fields{
|
||||
"mac": req.ClientHWAddr.String(),
|
||||
"autoconfigure": fmt.Sprintf("%v", ac),
|
||||
}).Debugf("Responded with autoconfigure %v", autoconfigure)
|
||||
return resp, false
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{
|
||||
"mac": req.ClientHWAddr.String(),
|
||||
"autoconfigure": "nil",
|
||||
}).Debugf("Client does not support autoconfigure")
|
||||
// RFC2563 2.3: if no address is chosen for the host [...]
|
||||
// If the DHCPDISCOVER does not contain the Auto-Configure option,
|
||||
// it is not answered.
|
||||
return nil, true
|
||||
}
|
||||
118
plugins/autoconfigure/plugin_test.go
Normal file
118
plugins/autoconfigure/plugin_test.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// 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 autoconfigure
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
func TestOptionRequested0(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionAutoConfigure, []byte{1}))
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req,
|
||||
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
opt := resp.Options.Get(dhcpv4.OptionAutoConfigure)
|
||||
if opt == nil {
|
||||
t.Fatal("plugin did not return the Auto-Configure option")
|
||||
}
|
||||
if !bytes.Equal(opt, []byte{0}) {
|
||||
t.Errorf("plugin gave wrong option response: %v", opt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionRequested1(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.UpdateOption(dhcpv4.OptGeneric(dhcpv4.OptionAutoConfigure, []byte{1}))
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req,
|
||||
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
autoconfigure = 1
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
opt := resp.Options.Get(dhcpv4.OptionAutoConfigure)
|
||||
if opt == nil {
|
||||
t.Fatal("plugin did not return the Auto-Configure option")
|
||||
}
|
||||
if !bytes.Equal(opt, []byte{1}) {
|
||||
t.Errorf("plugin gave wrong option response: %v", opt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotRequestedAssignedIP(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req,
|
||||
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub.YourIPAddr = net.ParseIP("192.0.2.100")
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
if resp.Options.Get(dhcpv4.OptionAutoConfigure) != nil {
|
||||
t.Error("plugin responsed with AutoConfigure option")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotRequestedNoIP(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req,
|
||||
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp != nil {
|
||||
t.Error("plugin returned a message")
|
||||
}
|
||||
if !stop {
|
||||
t.Error("plugin did not interrupt processing")
|
||||
}
|
||||
}
|
||||
83
plugins/dns/plugin.go
Normal file
83
plugins/dns/plugin.go
Normal file
@@ -0,0 +1,83 @@
|
||||
// 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 dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/dns")
|
||||
|
||||
// Plugin wraps the DNS plugin information.
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "dns",
|
||||
Setup6: setup6,
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
var (
|
||||
dnsServers6 []net.IP
|
||||
dnsServers4 []net.IP
|
||||
)
|
||||
|
||||
func setup6(args ...string) (handler.Handler6, error) {
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("need at least one DNS server")
|
||||
}
|
||||
for _, arg := range args {
|
||||
server := net.ParseIP(arg)
|
||||
if server.To16() == nil {
|
||||
return Handler6, errors.New("expected an DNS server address, got: " + arg)
|
||||
}
|
||||
dnsServers6 = append(dnsServers6, server)
|
||||
}
|
||||
log.Infof("loaded %d DNS servers.", len(dnsServers6))
|
||||
return Handler6, nil
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
log.Printf("loaded plugin for DHCPv4.")
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("need at least one DNS server")
|
||||
}
|
||||
for _, arg := range args {
|
||||
DNSServer := net.ParseIP(arg)
|
||||
if DNSServer.To4() == nil {
|
||||
return Handler4, errors.New("expected an DNS server address, got: " + arg)
|
||||
}
|
||||
dnsServers4 = append(dnsServers4, DNSServer)
|
||||
}
|
||||
log.Infof("loaded %d DNS servers.", len(dnsServers4))
|
||||
return Handler4, nil
|
||||
}
|
||||
|
||||
// Handler6 handles DHCPv6 packets for the dns plugin
|
||||
func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
|
||||
decap, err := req.GetInnerMessage()
|
||||
if err != nil {
|
||||
log.Errorf("Could not decapsulate relayed message, aborting: %v", err)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if decap.IsOptionRequested(dhcpv6.OptionDNSRecursiveNameServer) {
|
||||
resp.UpdateOption(dhcpv6.OptDNS(dnsServers6...))
|
||||
}
|
||||
return resp, false
|
||||
}
|
||||
|
||||
//Handler4 handles DHCPv4 packets for the dns plugin
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
if req.IsOptionRequested(dhcpv4.OptionDomainNameServer) {
|
||||
resp.Options.Update(dhcpv4.OptDNS(dnsServers4...))
|
||||
}
|
||||
return resp, false
|
||||
}
|
||||
149
plugins/dns/plugin_test.go
Normal file
149
plugins/dns/plugin_test.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// 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 dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
)
|
||||
|
||||
func TestAddServer6(t *testing.T) {
|
||||
req, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.MessageType = dhcpv6.MessageTypeRequest
|
||||
req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionDNSRecursiveNameServer))
|
||||
|
||||
stub, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub.MessageType = dhcpv6.MessageTypeReply
|
||||
|
||||
dnsServers6 = []net.IP{
|
||||
net.ParseIP("2001:db8::1"),
|
||||
net.ParseIP("2001:db8::3"),
|
||||
}
|
||||
|
||||
resp, stop := Handler6(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
opts := resp.GetOption(dhcpv6.OptionDNSRecursiveNameServer)
|
||||
if len(opts) != 1 {
|
||||
t.Fatalf("Expected 1 RDNSS option, got %d: %v", len(opts), opts)
|
||||
}
|
||||
foundServers := resp.(*dhcpv6.Message).Options.DNS()
|
||||
// XXX: is enforcing the order relevant here ?
|
||||
for i, srv := range foundServers {
|
||||
if !srv.Equal(dnsServers6[i]) {
|
||||
t.Errorf("Found server %s, expected %s", srv, dnsServers6[i])
|
||||
}
|
||||
}
|
||||
if len(foundServers) != len(dnsServers6) {
|
||||
t.Errorf("Found %d servers, expected %d", len(foundServers), len(dnsServers6))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotRequested6(t *testing.T) {
|
||||
req, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.MessageType = dhcpv6.MessageTypeRequest
|
||||
req.AddOption(dhcpv6.OptRequestedOption())
|
||||
|
||||
stub, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub.MessageType = dhcpv6.MessageTypeReply
|
||||
|
||||
dnsServers6 = []net.IP{
|
||||
net.ParseIP("2001:db8::1"),
|
||||
}
|
||||
|
||||
resp, stop := Handler6(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
|
||||
opts := resp.GetOption(dhcpv6.OptionDNSRecursiveNameServer)
|
||||
if len(opts) != 0 {
|
||||
t.Errorf("RDNSS options were added when not requested: %v", opts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddServer4(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dnsServers4 = []net.IP{
|
||||
net.ParseIP("192.0.2.1"),
|
||||
net.ParseIP("192.0.2.3"),
|
||||
}
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
servers := resp.DNS()
|
||||
for i, srv := range servers {
|
||||
if !srv.Equal(dnsServers4[i]) {
|
||||
t.Errorf("Found server %s, expected %s", srv, dnsServers4[i])
|
||||
}
|
||||
}
|
||||
if len(servers) != len(dnsServers4) {
|
||||
t.Errorf("Found %d servers, expected %d", len(servers), len(dnsServers4))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotRequested4(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
dnsServers4 = []net.IP{
|
||||
net.ParseIP("192.0.2.1"),
|
||||
}
|
||||
req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress))
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
servers := dhcpv4.GetIPs(dhcpv4.OptionDomainNameServer, resp.Options)
|
||||
if len(servers) != 0 {
|
||||
t.Errorf("Found %d DNS servers when explicitly not requested", len(servers))
|
||||
}
|
||||
}
|
||||
118
plugins/example/plugin.go
Normal file
118
plugins/example/plugin.go
Normal file
@@ -0,0 +1,118 @@
|
||||
// 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 example
|
||||
|
||||
// This is an example plugin that inspects a packet and prints it out. The code
|
||||
// is commented in a way that should walk you through the implementation of your
|
||||
// own plugins.
|
||||
// Feedback is welcome!
|
||||
|
||||
import (
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
)
|
||||
|
||||
// We use a customizable logger, as part of the `logger` package. You can use
|
||||
// `logger.GetLogger()` to get a singleton instance of the logger. Then just use
|
||||
// it with the `logrus` interface (https://github.com/sirupsen/logrus). More
|
||||
// information in the docstring of the logger package.
|
||||
var log = logger.GetLogger("plugins/example")
|
||||
|
||||
// Plugin wraps the information necessary to register a plugin.
|
||||
// In the main package, you need to export a `plugins.Plugin` object called
|
||||
// `Plugin`, so it can be registered into the plugin registry.
|
||||
// Just import your plugin, and fill the structure with plugin name and setup
|
||||
// functions:
|
||||
//
|
||||
// import (
|
||||
// "github.com/coredhcp/coredhcp/plugins"
|
||||
// "github.com/coredhcp/coredhcp/plugins/example"
|
||||
// )
|
||||
//
|
||||
// var Plugin = plugins.Plugin{
|
||||
// Name: "example",
|
||||
// Setup6: setup6,
|
||||
// Setup4: setup4,
|
||||
// }
|
||||
//
|
||||
// Name is simply the name used to register the plugin. It must be unique to
|
||||
// other registered plugins, or the operation will fail. In other words, don't
|
||||
// declare plugins with colliding names.
|
||||
//
|
||||
// Setup6 and Setup4 are the setup functions for DHCPv6 and DHCPv4 traffic
|
||||
// handlers. They conform to the `plugins.SetupFunc6` and `plugins.SetupFunc4`
|
||||
// interfaces, so they must return a `plugins.Handler6` and a `plugins.Handler4`
|
||||
// respectively.
|
||||
// A `nil` setup function means that that protocol won't be handled by this
|
||||
// plugin.
|
||||
//
|
||||
// Note that importing the plugin is not enough to use it: you have to
|
||||
// explicitly specify the intention to use it in the `config.yml` file, in the
|
||||
// plugins section. For example:
|
||||
//
|
||||
// server6:
|
||||
// listen: '[::]547'
|
||||
// - example:
|
||||
// - server_id: LL aa:bb:cc:dd:ee:ff
|
||||
// - file: "leases.txt"
|
||||
//
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "example",
|
||||
Setup6: setup6,
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
// setup6 is the setup function to initialize the handler for DHCPv6
|
||||
// traffic. This function implements the `plugin.SetupFunc6` interface.
|
||||
// This function returns a `handler.Handler6` function, and an error if any.
|
||||
// In this example we do very little in the setup function, and just return the
|
||||
// `exampleHandler6` function. Such function will be called for every DHCPv6
|
||||
// packet that the server receives. Remember that a handler may not be called
|
||||
// for each packet, if the handler chain is interrupted before reaching it.
|
||||
func setup6(args ...string) (handler.Handler6, error) {
|
||||
log.Printf("loaded plugin for DHCPv6.")
|
||||
return exampleHandler6, nil
|
||||
}
|
||||
|
||||
// setup4 behaves like setupExample6, but for DHCPv4 packets. It
|
||||
// implements the `plugin.SetupFunc4` interface.
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
log.Printf("loaded plugin for DHCPv4.")
|
||||
return exampleHandler4, nil
|
||||
}
|
||||
|
||||
// exampleHandler6 handles DHCPv6 packets for the example plugin. It implements
|
||||
// the `handler.Handler6` interface. The input arguments are the request packet
|
||||
// that the server received from a client, and the response packet that has been
|
||||
// computed so far. This function returns the response packet to be sent back to
|
||||
// the client, and a boolean.
|
||||
// The response can be either the same response packet received as input, a
|
||||
// modified response packet, or nil. If nil, the server will not reply to the
|
||||
// client, basically dropping the request.
|
||||
// The returned boolean indicates to the server whether the chain of plugins
|
||||
// should continue or not. If `true`, the server will stop at this plugin, and
|
||||
// respond to the client (or drop the response, if nil). If `false`, the server
|
||||
// will call the next plugin in the chan, using the returned response packet as
|
||||
// input for the next plugin.
|
||||
func exampleHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
|
||||
log.Printf("received DHCPv6 packet: %s", req.Summary())
|
||||
// return the unmodified response, and false. This means that the next
|
||||
// plugin in the chain will be called, and the unmodified response packet
|
||||
// will be used as its input.
|
||||
return resp, false
|
||||
}
|
||||
|
||||
// exampleHandler4 behaves like exampleHandler6, but for DHCPv4 packets. It
|
||||
// implements the `handler.Handler4` interface.
|
||||
func exampleHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
log.Printf("received DHCPv4 packet: %s", req.Summary())
|
||||
// return the unmodified response, and false. This means that the next
|
||||
// plugin in the chain will be called, and the unmodified response packet
|
||||
// will be used as its input.
|
||||
return resp, false
|
||||
}
|
||||
282
plugins/file/plugin.go
Normal file
282
plugins/file/plugin.go
Normal file
@@ -0,0 +1,282 @@
|
||||
// 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 file enables static mapping of MAC <--> IP addresses.
|
||||
// The mapping is stored in a text file, where each mapping is described by one line containing
|
||||
// two fields separated by spaces: MAC address, and IP address. For example:
|
||||
//
|
||||
// $ cat file_leases.txt
|
||||
// 00:11:22:33:44:55 10.0.0.1
|
||||
// 01:23:45:67:89:01 10.0.10.10
|
||||
//
|
||||
// To specify the plugin configuration in the server6/server4 sections of the config file, just
|
||||
// pass the leases file name as plugin argument, e.g.:
|
||||
//
|
||||
// $ cat config.yml
|
||||
//
|
||||
// server6:
|
||||
// ...
|
||||
// plugins:
|
||||
// - file: "file_leases.txt" [autorefresh]
|
||||
// ...
|
||||
//
|
||||
// If the file path is not absolute, it is relative to the cwd where coredhcp is run.
|
||||
//
|
||||
// Optionally, when the 'autorefresh' argument is given, the plugin will try to refresh
|
||||
// the lease mapping during runtime whenever the lease file is updated.
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
)
|
||||
|
||||
const (
|
||||
autoRefreshArg = "autorefresh"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/file")
|
||||
|
||||
// Plugin wraps plugin registration information
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "file",
|
||||
Setup6: setup6,
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
var recLock sync.RWMutex
|
||||
|
||||
// StaticRecords holds a MAC -> IP address mapping
|
||||
var StaticRecords map[string]net.IP
|
||||
|
||||
// DHCPv6Records and DHCPv4Records are mappings between MAC addresses in
|
||||
// form of a string, to network configurations.
|
||||
var (
|
||||
DHCPv6Records map[string]net.IP
|
||||
DHCPv4Records map[string]net.IP
|
||||
)
|
||||
|
||||
// LoadDHCPv4Records loads the DHCPv4Records global map with records stored on
|
||||
// the specified file. The records have to be one per line, a mac address and an
|
||||
// IPv4 address.
|
||||
func LoadDHCPv4Records(filename string) (map[string]net.IP, error) {
|
||||
log.Infof("reading leases from %s", filename)
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records := make(map[string]net.IP)
|
||||
for _, lineBytes := range bytes.Split(data, []byte{'\n'}) {
|
||||
line := string(lineBytes)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
tokens := strings.Fields(line)
|
||||
if len(tokens) != 2 {
|
||||
return nil, fmt.Errorf("malformed line, want 2 fields, got %d: %s", len(tokens), line)
|
||||
}
|
||||
hwaddr, err := net.ParseMAC(tokens[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed hardware address: %s", tokens[0])
|
||||
}
|
||||
ipaddr := net.ParseIP(tokens[1])
|
||||
if ipaddr.To4() == nil {
|
||||
return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr)
|
||||
}
|
||||
records[hwaddr.String()] = ipaddr
|
||||
}
|
||||
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// LoadDHCPv6Records loads the DHCPv6Records global map with records stored on
|
||||
// the specified file. The records have to be one per line, a mac address and an
|
||||
// IPv6 address.
|
||||
func LoadDHCPv6Records(filename string) (map[string]net.IP, error) {
|
||||
log.Infof("reading leases from %s", filename)
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records := make(map[string]net.IP)
|
||||
for _, lineBytes := range bytes.Split(data, []byte{'\n'}) {
|
||||
line := string(lineBytes)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
tokens := strings.Fields(line)
|
||||
if len(tokens) != 2 {
|
||||
return nil, fmt.Errorf("malformed line, want 2 fields, got %d: %s", len(tokens), line)
|
||||
}
|
||||
hwaddr, err := net.ParseMAC(tokens[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed hardware address: %s", tokens[0])
|
||||
}
|
||||
ipaddr := net.ParseIP(tokens[1])
|
||||
if ipaddr.To16() == nil || ipaddr.To4() != nil {
|
||||
return nil, fmt.Errorf("expected an IPv6 address, got: %v", ipaddr)
|
||||
}
|
||||
records[hwaddr.String()] = ipaddr
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// Handler6 handles DHCPv6 packets for the file plugin
|
||||
func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
|
||||
m, err := req.GetInnerMessage()
|
||||
if err != nil {
|
||||
log.Errorf("BUG: could not decapsulate: %v", err)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if m.Options.OneIANA() == nil {
|
||||
log.Debug("No address requested")
|
||||
return resp, false
|
||||
}
|
||||
|
||||
mac, err := dhcpv6.ExtractMAC(req)
|
||||
if err != nil {
|
||||
log.Warningf("Could not find client MAC, passing")
|
||||
return resp, false
|
||||
}
|
||||
log.Debugf("looking up an IP address for MAC %s", mac.String())
|
||||
|
||||
recLock.RLock()
|
||||
defer recLock.RUnlock()
|
||||
|
||||
ipaddr, ok := StaticRecords[mac.String()]
|
||||
if !ok {
|
||||
log.Warningf("MAC address %s is unknown", mac.String())
|
||||
return resp, false
|
||||
}
|
||||
log.Debugf("found IP address %s for MAC %s", ipaddr, mac.String())
|
||||
|
||||
resp.AddOption(&dhcpv6.OptIANA{
|
||||
IaId: m.Options.OneIANA().IaId,
|
||||
Options: dhcpv6.IdentityOptions{Options: []dhcpv6.Option{
|
||||
&dhcpv6.OptIAAddress{
|
||||
IPv6Addr: ipaddr,
|
||||
PreferredLifetime: 3600 * time.Second,
|
||||
ValidLifetime: 3600 * time.Second,
|
||||
},
|
||||
}},
|
||||
})
|
||||
return resp, false
|
||||
}
|
||||
|
||||
// Handler4 handles DHCPv4 packets for the file plugin
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
recLock.RLock()
|
||||
defer recLock.RUnlock()
|
||||
|
||||
ipaddr, ok := StaticRecords[req.ClientHWAddr.String()]
|
||||
if !ok {
|
||||
log.Warningf("MAC address %s is unknown", req.ClientHWAddr.String())
|
||||
return resp, false
|
||||
}
|
||||
resp.YourIPAddr = ipaddr
|
||||
log.Debugf("found IP address %s for MAC %s", ipaddr, req.ClientHWAddr.String())
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func setup6(args ...string) (handler.Handler6, error) {
|
||||
h6, _, err := setupFile(true, args...)
|
||||
return h6, err
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
_, h4, err := setupFile(false, args...)
|
||||
return h4, err
|
||||
}
|
||||
|
||||
func setupFile(v6 bool, args ...string) (handler.Handler6, handler.Handler4, error) {
|
||||
var err error
|
||||
if len(args) < 1 {
|
||||
return nil, nil, errors.New("need a file name")
|
||||
}
|
||||
filename := args[0]
|
||||
if filename == "" {
|
||||
return nil, nil, errors.New("got empty file name")
|
||||
}
|
||||
|
||||
// load initial database from lease file
|
||||
if err = loadFromFile(v6, filename); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// when the 'autorefresh' argument was passed, watch the lease file for
|
||||
// changes and reload the lease mapping on any event
|
||||
if len(args) > 1 && args[1] == autoRefreshArg {
|
||||
// creates a new file watcher
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to create watcher: %w", err)
|
||||
}
|
||||
|
||||
// have file watcher watch over lease file
|
||||
if err = watcher.Add(filename); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to watch %s: %w", filename, err)
|
||||
}
|
||||
|
||||
// very simple watcher on the lease file to trigger a refresh on any event
|
||||
// on the file
|
||||
go func() {
|
||||
for range watcher.Events {
|
||||
err := loadFromFile(v6, filename)
|
||||
if err != nil {
|
||||
log.Warningf("failed to refresh from %s: %s", filename, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("updated to %d leases from %s", len(StaticRecords), filename)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
log.Infof("loaded %d leases from %s", len(StaticRecords), filename)
|
||||
return Handler6, Handler4, nil
|
||||
}
|
||||
|
||||
func loadFromFile(v6 bool, filename string) error {
|
||||
var err error
|
||||
var records map[string]net.IP
|
||||
var protver int
|
||||
if v6 {
|
||||
protver = 6
|
||||
records, err = LoadDHCPv6Records(filename)
|
||||
} else {
|
||||
protver = 4
|
||||
records, err = LoadDHCPv4Records(filename)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load DHCPv%d records: %w", protver, err)
|
||||
}
|
||||
|
||||
recLock.Lock()
|
||||
defer recLock.Unlock()
|
||||
|
||||
StaticRecords = records
|
||||
|
||||
return nil
|
||||
}
|
||||
368
plugins/file/plugin_test.go
Normal file
368
plugins/file/plugin_test.go
Normal file
@@ -0,0 +1,368 @@
|
||||
// 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 file
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadDHCPv4Records(t *testing.T) {
|
||||
t.Run("valid leases", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// fill temp file with valid lease lines and some comments
|
||||
_, err = tmp.WriteString("00:11:22:33:44:55 192.0.2.100\n")
|
||||
require.NoError(t, err)
|
||||
_, err = tmp.WriteString("11:22:33:44:55:66 192.0.2.101\n")
|
||||
require.NoError(t, err)
|
||||
_, err = tmp.WriteString("# this is a comment\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
records, err := LoadDHCPv4Records(tmp.Name())
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if assert.Equal(t, 2, len(records)) {
|
||||
if assert.Contains(t, records, "00:11:22:33:44:55") {
|
||||
assert.Equal(t, net.ParseIP("192.0.2.100"), records["00:11:22:33:44:55"])
|
||||
}
|
||||
if assert.Contains(t, records, "11:22:33:44:55:66") {
|
||||
assert.Equal(t, net.ParseIP("192.0.2.101"), records["11:22:33:44:55:66"])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing field", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// add line with too few fields
|
||||
_, err = tmp.WriteString("foo\n")
|
||||
require.NoError(t, err)
|
||||
_, err = LoadDHCPv4Records(tmp.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid MAC", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// add line with invalid MAC address to trigger an error
|
||||
_, err = tmp.WriteString("abcd 192.0.2.102\n")
|
||||
require.NoError(t, err)
|
||||
_, err = LoadDHCPv4Records(tmp.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid IP address", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// add line with invalid MAC address to trigger an error
|
||||
_, err = tmp.WriteString("22:33:44:55:66:77 bcde\n")
|
||||
require.NoError(t, err)
|
||||
_, err = LoadDHCPv4Records(tmp.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("lease with IPv6 address", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// add line with IPv6 address instead to trigger an error
|
||||
_, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n")
|
||||
require.NoError(t, err)
|
||||
_, err = LoadDHCPv4Records(tmp.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadDHCPv6Records(t *testing.T) {
|
||||
t.Run("valid leases", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// fill temp file with valid lease lines and some comments
|
||||
_, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n")
|
||||
require.NoError(t, err)
|
||||
_, err = tmp.WriteString("11:22:33:44:55:66 2001:db8::10:2\n")
|
||||
require.NoError(t, err)
|
||||
_, err = tmp.WriteString("# this is a comment\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
records, err := LoadDHCPv6Records(tmp.Name())
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
if assert.Equal(t, 2, len(records)) {
|
||||
if assert.Contains(t, records, "00:11:22:33:44:55") {
|
||||
assert.Equal(t, net.ParseIP("2001:db8::10:1"), records["00:11:22:33:44:55"])
|
||||
}
|
||||
if assert.Contains(t, records, "11:22:33:44:55:66") {
|
||||
assert.Equal(t, net.ParseIP("2001:db8::10:2"), records["11:22:33:44:55:66"])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing field", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// add line with too few fields
|
||||
_, err = tmp.WriteString("foo\n")
|
||||
require.NoError(t, err)
|
||||
_, err = LoadDHCPv6Records(tmp.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid MAC", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// add line with invalid MAC address to trigger an error
|
||||
_, err = tmp.WriteString("abcd 2001:db8::10:3\n")
|
||||
require.NoError(t, err)
|
||||
_, err = LoadDHCPv6Records(tmp.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("invalid IP address", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// add line with invalid MAC address to trigger an error
|
||||
_, err = tmp.WriteString("22:33:44:55:66:77 bcde\n")
|
||||
require.NoError(t, err)
|
||||
_, err = LoadDHCPv6Records(tmp.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("lease with IPv4 address", func(t *testing.T) {
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
// add line with IPv4 address instead to trigger an error
|
||||
_, err = tmp.WriteString("00:11:22:33:44:55 192.0.2.100\n")
|
||||
require.NoError(t, err)
|
||||
_, err = LoadDHCPv6Records(tmp.Name())
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandler4(t *testing.T) {
|
||||
t.Run("unknown MAC", func(t *testing.T) {
|
||||
// prepare DHCPv4 request
|
||||
mac := "00:11:22:33:44:55"
|
||||
claddr, _ := net.ParseMAC(mac)
|
||||
req := &dhcpv4.DHCPv4{
|
||||
ClientHWAddr: claddr,
|
||||
}
|
||||
resp := &dhcpv4.DHCPv4{}
|
||||
assert.Nil(t, resp.ClientIPAddr)
|
||||
|
||||
// if we handle this DHCP request, nothing should change since the lease is
|
||||
// unknown
|
||||
result, stop := Handler4(req, resp)
|
||||
assert.Same(t, result, resp)
|
||||
assert.False(t, stop)
|
||||
assert.Nil(t, result.YourIPAddr)
|
||||
})
|
||||
|
||||
t.Run("known MAC", func(t *testing.T) {
|
||||
// prepare DHCPv4 request
|
||||
mac := "00:11:22:33:44:55"
|
||||
claddr, _ := net.ParseMAC(mac)
|
||||
req := &dhcpv4.DHCPv4{
|
||||
ClientHWAddr: claddr,
|
||||
}
|
||||
resp := &dhcpv4.DHCPv4{}
|
||||
assert.Nil(t, resp.ClientIPAddr)
|
||||
|
||||
// add lease for the MAC in the lease map
|
||||
clIPAddr := net.ParseIP("192.0.2.100")
|
||||
StaticRecords = map[string]net.IP{
|
||||
mac: clIPAddr,
|
||||
}
|
||||
|
||||
// if we handle this DHCP request, the YourIPAddr field should be set
|
||||
// in the result
|
||||
result, stop := Handler4(req, resp)
|
||||
assert.Same(t, result, resp)
|
||||
assert.True(t, stop)
|
||||
assert.Equal(t, clIPAddr, result.YourIPAddr)
|
||||
|
||||
// cleanup
|
||||
StaticRecords = make(map[string]net.IP)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandler6(t *testing.T) {
|
||||
t.Run("unknown MAC", func(t *testing.T) {
|
||||
// prepare DHCPv6 request
|
||||
mac := "11:22:33:44:55:66"
|
||||
claddr, _ := net.ParseMAC(mac)
|
||||
req, err := dhcpv6.NewSolicit(claddr)
|
||||
require.NoError(t, err)
|
||||
resp, err := dhcpv6.NewAdvertiseFromSolicit(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(resp.GetOption(dhcpv6.OptionIANA)))
|
||||
|
||||
// if we handle this DHCP request, nothing should change since the lease is
|
||||
// unknown
|
||||
result, stop := Handler6(req, resp)
|
||||
assert.False(t, stop)
|
||||
assert.Equal(t, 0, len(result.GetOption(dhcpv6.OptionIANA)))
|
||||
})
|
||||
|
||||
t.Run("known MAC", func(t *testing.T) {
|
||||
// prepare DHCPv6 request
|
||||
mac := "11:22:33:44:55:66"
|
||||
claddr, _ := net.ParseMAC(mac)
|
||||
req, err := dhcpv6.NewSolicit(claddr)
|
||||
require.NoError(t, err)
|
||||
resp, err := dhcpv6.NewAdvertiseFromSolicit(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(resp.GetOption(dhcpv6.OptionIANA)))
|
||||
|
||||
// add lease for the MAC in the lease map
|
||||
clIPAddr := net.ParseIP("2001:db8::10:1")
|
||||
StaticRecords = map[string]net.IP{
|
||||
mac: clIPAddr,
|
||||
}
|
||||
|
||||
// if we handle this DHCP request, there should be a specific IANA option
|
||||
// set in the resulting response
|
||||
result, stop := Handler6(req, resp)
|
||||
assert.False(t, stop)
|
||||
if assert.Equal(t, 1, len(result.GetOption(dhcpv6.OptionIANA))) {
|
||||
opt := result.GetOneOption(dhcpv6.OptionIANA)
|
||||
assert.Contains(t, opt.String(), "IP=2001:db8::10:1")
|
||||
}
|
||||
|
||||
// cleanup
|
||||
StaticRecords = make(map[string]net.IP)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetupFile(t *testing.T) {
|
||||
// too few arguments
|
||||
_, _, err := setupFile(false)
|
||||
assert.Error(t, err)
|
||||
|
||||
// empty file name
|
||||
_, _, err = setupFile(false, "")
|
||||
assert.Error(t, err)
|
||||
|
||||
// trigger error in LoadDHCPv*Records
|
||||
_, _, err = setupFile(false, "/foo/bar")
|
||||
assert.Error(t, err)
|
||||
|
||||
_, _, err = setupFile(true, "/foo/bar")
|
||||
assert.Error(t, err)
|
||||
|
||||
// setup temp leases file
|
||||
tmp, err := os.CreateTemp("", "test_plugin_file")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmp.Name())
|
||||
}()
|
||||
|
||||
t.Run("typical case", func(t *testing.T) {
|
||||
_, err = tmp.WriteString("00:11:22:33:44:55 2001:db8::10:1\n")
|
||||
require.NoError(t, err)
|
||||
_, err = tmp.WriteString("11:22:33:44:55:66 2001:db8::10:2\n")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 0, len(StaticRecords))
|
||||
|
||||
// leases should show up in StaticRecords
|
||||
_, _, err = setupFile(true, tmp.Name())
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, 2, len(StaticRecords))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("autorefresh enabled", func(t *testing.T) {
|
||||
_, _, err = setupFile(true, tmp.Name(), autoRefreshArg)
|
||||
if assert.NoError(t, err) {
|
||||
assert.Equal(t, 2, len(StaticRecords))
|
||||
}
|
||||
// we add more leases to the file
|
||||
// this should trigger an event to refresh the leases database
|
||||
// without calling setupFile again
|
||||
_, err = tmp.WriteString("22:33:44:55:66:77 2001:db8::10:3\n")
|
||||
require.NoError(t, err)
|
||||
// since the event is processed asynchronously, give it a little time
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
// an additional record should show up in the database
|
||||
// but we should respect the locking first
|
||||
recLock.RLock()
|
||||
defer recLock.RUnlock()
|
||||
|
||||
assert.Equal(t, 3, len(StaticRecords))
|
||||
})
|
||||
}
|
||||
64
plugins/ipv6only/plugin.go
Normal file
64
plugins/ipv6only/plugin.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// 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 ipv6only
|
||||
|
||||
// This plugin implements RFC8925: if the client has requested the
|
||||
// IPv6-Only Preferred option, then add the option response and then
|
||||
// terminate processing immediately.
|
||||
//
|
||||
// This module should be invoked *before* any IP address
|
||||
// allocation has been done, so that the yiaddr is 0.0.0.0 and
|
||||
// no pool addresses are consumed for compatible clients.
|
||||
//
|
||||
// The optional argument is the V6ONLY_WAIT configuration variable,
|
||||
// described in RFC8925 section 3.2.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/ipv6only")
|
||||
|
||||
var v6only_wait time.Duration
|
||||
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "ipv6only",
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
if len(args) > 0 {
|
||||
dur, err := time.ParseDuration(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("invalid duration: %v", args[0])
|
||||
return nil, errors.New("ipv6only failed to initialize")
|
||||
}
|
||||
v6only_wait = dur
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return nil, errors.New("too many arguments")
|
||||
}
|
||||
return Handler4, nil
|
||||
}
|
||||
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
v6pref := req.IsOptionRequested(dhcpv4.OptionIPv6OnlyPreferred)
|
||||
log.WithFields(logrus.Fields{
|
||||
"mac": req.ClientHWAddr.String(),
|
||||
"ipv6only": v6pref,
|
||||
}).Debug("ipv6only status")
|
||||
if v6pref {
|
||||
resp.UpdateOption(dhcpv4.OptIPv6OnlyPreferred(v6only_wait))
|
||||
return resp, true
|
||||
}
|
||||
return resp, false
|
||||
}
|
||||
65
plugins/ipv6only/plugin_test.go
Normal file
65
plugins/ipv6only/plugin_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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 ipv6only
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
func TestOptionRequested(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress, dhcpv4.OptionIPv6OnlyPreferred))
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
v6only_wait = 0x1234 * time.Second
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if !stop {
|
||||
t.Error("plugin did not interrupt processing")
|
||||
}
|
||||
opt := resp.Options.Get(dhcpv4.OptionIPv6OnlyPreferred)
|
||||
if opt == nil {
|
||||
t.Fatal("plugin did not return the IPv6-Only Preferred option")
|
||||
}
|
||||
if !bytes.Equal(opt, []byte{0x00, 0x00, 0x12, 0x34}) {
|
||||
t.Errorf("plugin gave wrong option response: %v", opt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotRequested(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
if resp.Options.Get(dhcpv4.OptionIPv6OnlyPreferred) != nil {
|
||||
t.Error("Found IPv6-Only Preferred option when not requested")
|
||||
}
|
||||
}
|
||||
57
plugins/leasetime/plugin.go
Normal file
57
plugins/leasetime/plugin.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// 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 leasetime
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
// Plugin wraps plugin registration information
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "lease_time",
|
||||
// currently not supported for DHCPv6
|
||||
Setup6: nil,
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
var (
|
||||
log = logger.GetLogger("plugins/lease_time")
|
||||
v4LeaseTime time.Duration
|
||||
)
|
||||
|
||||
// Handler4 handles DHCPv4 packets for the lease_time plugin.
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
if req.OpCode != dhcpv4.OpcodeBootRequest {
|
||||
return resp, false
|
||||
}
|
||||
// Set lease time unless it has already been set
|
||||
if !resp.Options.Has(dhcpv4.OptionIPAddressLeaseTime) {
|
||||
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(v4LeaseTime))
|
||||
}
|
||||
return resp, false
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
log.Print("loading `lease_time` plugin for DHCPv4")
|
||||
if len(args) < 1 {
|
||||
log.Error("No default lease time provided")
|
||||
return nil, errors.New("lease_time failed to initialize")
|
||||
}
|
||||
|
||||
leaseTime, err := time.ParseDuration(args[0])
|
||||
if err != nil {
|
||||
log.Errorf("invalid duration: %v", args[0])
|
||||
return nil, errors.New("lease_time failed to initialize")
|
||||
}
|
||||
v4LeaseTime = leaseTime
|
||||
|
||||
return Handler4, nil
|
||||
}
|
||||
50
plugins/mtu/plugin.go
Normal file
50
plugins/mtu/plugin.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// 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 mtu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/mtu")
|
||||
|
||||
// Plugin wraps the MTU plugin information.
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "mtu",
|
||||
Setup4: setup4,
|
||||
// No Setup6 since DHCPv6 does not have MTU-related options
|
||||
}
|
||||
|
||||
var (
|
||||
mtu int
|
||||
)
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, errors.New("need one mtu value")
|
||||
}
|
||||
var err error
|
||||
if mtu, err = strconv.Atoi(args[0]); err != nil {
|
||||
return nil, fmt.Errorf("invalid mtu: %v", args[0])
|
||||
}
|
||||
log.Infof("loaded mtu %d.", mtu)
|
||||
return Handler4, nil
|
||||
}
|
||||
|
||||
// Handler4 handles DHCPv4 packets for the mtu plugin
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
if req.IsOptionRequested(dhcpv4.OptionInterfaceMTU) {
|
||||
resp.Options.Update(dhcpv4.Option{Code: dhcpv4.OptionInterfaceMTU, Value: dhcpv4.Uint16(mtu)})
|
||||
}
|
||||
return resp, false
|
||||
}
|
||||
66
plugins/mtu/plugin_test.go
Normal file
66
plugins/mtu/plugin_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// 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 mtu
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
func TestAddServer4(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, dhcpv4.WithRequestedOptions(dhcpv4.OptionInterfaceMTU))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mtu = 1500
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
rMTU, err := dhcpv4.GetUint16(dhcpv4.OptionInterfaceMTU, resp.Options)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to retrieve mtu from response")
|
||||
}
|
||||
|
||||
if mtu != int(rMTU) {
|
||||
t.Errorf("Found %d mtu, expected %d", rMTU, mtu)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotRequested4(t *testing.T) {
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
mtu = 1500
|
||||
req.UpdateOption(dhcpv4.OptParameterRequestList(dhcpv4.OptionBroadcastAddress))
|
||||
|
||||
resp, stop := Handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
if mtu, err := dhcpv4.GetUint16(dhcpv4.OptionInterfaceMTU, resp.Options); err == nil {
|
||||
t.Errorf("Retrieve mtu %d in response, expected none", mtu)
|
||||
}
|
||||
}
|
||||
142
plugins/nbp/nbp.go
Normal file
142
plugins/nbp/nbp.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// 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 nbp implements handling of an NBP (Network Boot Program) using an
|
||||
// URL, e.g. http://[fe80::abcd:efff:fe12:3456]/my-nbp or tftp://10.0.0.1/my-nbp .
|
||||
// The NBP information is only added if it is requested by the client.
|
||||
//
|
||||
// Note that for DHCPv4, unless the URL is prefixed with a "http", "https" or
|
||||
// "ftp" scheme, the URL will be split into TFTP server name (option 66)
|
||||
// and Bootfile name (option 67), so the scheme will be stripped out, and it
|
||||
// will be treated as a TFTP URL. Anything other than host name and file path
|
||||
// will be ignored (no port, no query string, etc).
|
||||
//
|
||||
// For DHCPv6 OPT_BOOTFILE_URL (option 59) is used, and the value is passed
|
||||
// unmodified. If the query string is specified and contains a "param" key,
|
||||
// its value is also passed as OPT_BOOTFILE_PARAM (option 60), so it will be
|
||||
// duplicated between option 59 and 60.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// server6:
|
||||
// - plugins:
|
||||
// - nbp: http://[2001:db8:a::1]/nbp
|
||||
//
|
||||
// server4:
|
||||
// - plugins:
|
||||
// - nbp: tftp://10.0.0.254/nbp
|
||||
//
|
||||
package nbp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/nbp")
|
||||
|
||||
// Plugin wraps plugin registration information
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "nbp",
|
||||
Setup6: setup6,
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
var (
|
||||
opt59, opt60 dhcpv6.Option
|
||||
opt66, opt67 *dhcpv4.Option
|
||||
)
|
||||
|
||||
func parseArgs(args ...string) (*url.URL, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, fmt.Errorf("Exactly one argument must be passed to NBP plugin, got %d", len(args))
|
||||
}
|
||||
return url.Parse(args[0])
|
||||
}
|
||||
|
||||
func setup6(args ...string) (handler.Handler6, error) {
|
||||
u, err := parseArgs(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opt59 = dhcpv6.OptBootFileURL(u.String())
|
||||
params := u.Query().Get("params")
|
||||
if params != "" {
|
||||
opt60 = &dhcpv6.OptionGeneric{
|
||||
OptionCode: dhcpv6.OptionBootfileParam,
|
||||
OptionData: []byte(params),
|
||||
}
|
||||
}
|
||||
log.Printf("loaded NBP plugin for DHCPv6.")
|
||||
return nbpHandler6, nil
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
u, err := parseArgs(args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var otsn, obfn dhcpv4.Option
|
||||
switch u.Scheme {
|
||||
case "http", "https", "ftp":
|
||||
obfn = dhcpv4.OptBootFileName(u.String())
|
||||
default:
|
||||
otsn = dhcpv4.OptTFTPServerName(u.Host)
|
||||
obfn = dhcpv4.OptBootFileName(u.Path)
|
||||
opt66 = &otsn
|
||||
}
|
||||
|
||||
opt67 = &obfn
|
||||
log.Printf("loaded NBP plugin for DHCPv4.")
|
||||
return nbpHandler4, nil
|
||||
}
|
||||
|
||||
func nbpHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
|
||||
if opt59 == nil {
|
||||
// nothing to do
|
||||
return resp, true
|
||||
}
|
||||
decap, err := req.GetInnerMessage()
|
||||
if err != nil {
|
||||
log.Errorf("Could not decapsulate request: %v", err)
|
||||
// drop the request, this is probably a critical error in the packet.
|
||||
return nil, true
|
||||
}
|
||||
for _, code := range decap.Options.RequestedOptions() {
|
||||
if code == dhcpv6.OptionBootfileURL {
|
||||
// bootfile URL is requested
|
||||
resp.AddOption(opt59)
|
||||
} else if code == dhcpv6.OptionBootfileParam {
|
||||
// optionally add opt60, bootfile params, if requested
|
||||
if opt60 != nil {
|
||||
resp.AddOption(opt60)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Debugf("Added NBP %s to request", opt59)
|
||||
return resp, true
|
||||
}
|
||||
|
||||
func nbpHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
if opt67 == nil {
|
||||
// nothing to do
|
||||
return resp, true
|
||||
}
|
||||
if req.IsOptionRequested(dhcpv4.OptionTFTPServerName) && opt66 != nil {
|
||||
resp.Options.Update(*opt66)
|
||||
log.Debugf("Added NBP %s / %s to request", opt66, opt67)
|
||||
}
|
||||
if req.IsOptionRequested(dhcpv4.OptionBootfileName) {
|
||||
resp.Options.Update(*opt67)
|
||||
log.Debugf("Added NBP %s to request", opt67)
|
||||
}
|
||||
return resp, true
|
||||
}
|
||||
62
plugins/netmask/plugin.go
Normal file
62
plugins/netmask/plugin.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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 netmask
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/netmask")
|
||||
|
||||
// Plugin wraps plugin registration information
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "netmask",
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
var (
|
||||
netmask net.IPMask
|
||||
)
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
log.Printf("loaded plugin for DHCPv4.")
|
||||
if len(args) != 1 {
|
||||
return nil, errors.New("need at least one netmask IP address")
|
||||
}
|
||||
netmaskIP := net.ParseIP(args[0])
|
||||
if netmaskIP.IsUnspecified() {
|
||||
return nil, errors.New("netmask is not valid, got: " + args[0])
|
||||
}
|
||||
netmaskIP = netmaskIP.To4()
|
||||
if netmaskIP == nil {
|
||||
return nil, errors.New("expected an netmask address, got: " + args[0])
|
||||
}
|
||||
netmask = net.IPv4Mask(netmaskIP[0], netmaskIP[1], netmaskIP[2], netmaskIP[3])
|
||||
if !checkValidNetmask(netmask) {
|
||||
return nil, errors.New("netmask is not valid, got: " + args[0])
|
||||
}
|
||||
log.Printf("loaded client netmask")
|
||||
return Handler4, nil
|
||||
}
|
||||
|
||||
//Handler4 handles DHCPv4 packets for the netmask plugin
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
resp.Options.Update(dhcpv4.OptSubnetMask(netmask))
|
||||
return resp, false
|
||||
}
|
||||
|
||||
func checkValidNetmask(netmask net.IPMask) bool {
|
||||
netmaskInt := binary.BigEndian.Uint32(netmask)
|
||||
x := ^netmaskInt
|
||||
y := x + 1
|
||||
return (y & x) == 0
|
||||
}
|
||||
65
plugins/netmask/plugin_test.go
Normal file
65
plugins/netmask/plugin_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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 netmask
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckValidNetmask(t *testing.T) {
|
||||
assert.True(t, checkValidNetmask(net.IPv4Mask(255, 255, 255, 0)))
|
||||
assert.True(t, checkValidNetmask(net.IPv4Mask(255, 255, 0, 0)))
|
||||
assert.True(t, checkValidNetmask(net.IPv4Mask(255, 0, 0, 0)))
|
||||
assert.True(t, checkValidNetmask(net.IPv4Mask(0, 0, 0, 0)))
|
||||
|
||||
assert.False(t, checkValidNetmask(net.IPv4Mask(0, 255, 255, 255)))
|
||||
assert.False(t, checkValidNetmask(net.IPv4Mask(0, 0, 255, 255)))
|
||||
assert.False(t, checkValidNetmask(net.IPv4Mask(0, 0, 0, 255)))
|
||||
}
|
||||
|
||||
func TestHandler4(t *testing.T) {
|
||||
// set plugin netmask
|
||||
netmask = net.IPv4Mask(255, 255, 255, 0)
|
||||
|
||||
// prepare DHCPv4 request
|
||||
req := &dhcpv4.DHCPv4{}
|
||||
resp := &dhcpv4.DHCPv4{
|
||||
Options: dhcpv4.Options{},
|
||||
}
|
||||
|
||||
// if we handle this DHCP request, the netmask should be one of the options
|
||||
// of the result
|
||||
result, stop := Handler4(req, resp)
|
||||
assert.Same(t, result, resp)
|
||||
assert.False(t, stop)
|
||||
assert.EqualValues(t, netmask, resp.Options.Get(dhcpv4.OptionSubnetMask))
|
||||
}
|
||||
|
||||
func TestSetup4(t *testing.T) {
|
||||
// valid configuration
|
||||
_, err := setup4("255.255.255.0")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, netmask, net.IPv4Mask(255, 255, 255, 0))
|
||||
|
||||
// no configuration
|
||||
_, err = setup4()
|
||||
assert.Error(t, err)
|
||||
|
||||
// unspecified netmask
|
||||
_, err = setup4("0.0.0.0")
|
||||
assert.Error(t, err)
|
||||
|
||||
// ipv6 prefix
|
||||
_, err = setup4("ff02::/64")
|
||||
assert.Error(t, err)
|
||||
|
||||
// invalid netmask
|
||||
_, err = setup4("0.0.0.255")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
114
plugins/plugin.go
Normal file
114
plugins/plugin.go
Normal file
@@ -0,0 +1,114 @@
|
||||
// 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 plugins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/coredhcp/coredhcp/config"
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins")
|
||||
|
||||
// Plugin represents a plugin object.
|
||||
// Setup6 and Setup4 are the setup functions for DHCPv6 and DHCPv4 handlers
|
||||
// respectively. Both setup functions can be nil.
|
||||
type Plugin struct {
|
||||
Name string
|
||||
Setup6 SetupFunc6
|
||||
Setup4 SetupFunc4
|
||||
}
|
||||
|
||||
// RegisteredPlugins maps a plugin name to a Plugin instance.
|
||||
var RegisteredPlugins = make(map[string]*Plugin)
|
||||
|
||||
// SetupFunc6 defines a plugin setup function for DHCPv6
|
||||
type SetupFunc6 func(args ...string) (handler.Handler6, error)
|
||||
|
||||
// SetupFunc4 defines a plugin setup function for DHCPv6
|
||||
type SetupFunc4 func(args ...string) (handler.Handler4, error)
|
||||
|
||||
// RegisterPlugin registers a plugin.
|
||||
func RegisterPlugin(plugin *Plugin) error {
|
||||
if plugin == nil {
|
||||
return errors.New("cannot register nil plugin")
|
||||
}
|
||||
log.Printf("Registering plugin '%s'", plugin.Name)
|
||||
if _, ok := RegisteredPlugins[plugin.Name]; ok {
|
||||
// TODO this highlights that asking the plugins to register themselves
|
||||
// is not the right approach. Need to register them in the main program.
|
||||
log.Panicf("Plugin '%s' is already registered", plugin.Name)
|
||||
}
|
||||
RegisteredPlugins[plugin.Name] = plugin
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadPlugins reads a Config object and loads the plugins as specified in the
|
||||
// `plugins` section, in order. For a plugin to be available, it must have been
|
||||
// previously registered with plugins.RegisterPlugin. This is normally done at
|
||||
// plugin import time.
|
||||
// This function returns the list of loaded v6 plugins, the list of loaded v4
|
||||
// plugins, and an error if any.
|
||||
func LoadPlugins(conf *config.Config) ([]handler.Handler4, []handler.Handler6, error) {
|
||||
log.Print("Loading plugins...")
|
||||
handlers4 := make([]handler.Handler4, 0)
|
||||
handlers6 := make([]handler.Handler6, 0)
|
||||
|
||||
if conf.Server6 == nil && conf.Server4 == nil {
|
||||
return nil, nil, errors.New("no configuration found for either DHCPv6 or DHCPv4")
|
||||
}
|
||||
|
||||
// now load the plugins. We need to call its setup function with
|
||||
// the arguments extracted above. The setup function is mapped in
|
||||
// plugins.RegisteredPlugins .
|
||||
|
||||
// Load DHCPv6 plugins.
|
||||
if conf.Server6 != nil {
|
||||
for _, pluginConf := range conf.Server6.Plugins {
|
||||
if plugin, ok := RegisteredPlugins[pluginConf.Name]; ok {
|
||||
log.Printf("DHCPv6: loading plugin `%s`", pluginConf.Name)
|
||||
if plugin.Setup6 == nil {
|
||||
log.Warningf("DHCPv6: plugin `%s` has no setup function for DHCPv6", pluginConf.Name)
|
||||
continue
|
||||
}
|
||||
h6, err := plugin.Setup6(pluginConf.Args...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if h6 == nil {
|
||||
return nil, nil, config.ConfigErrorFromString("no DHCPv6 handler for plugin %s", pluginConf.Name)
|
||||
}
|
||||
handlers6 = append(handlers6, h6)
|
||||
} else {
|
||||
return nil, nil, config.ConfigErrorFromString("DHCPv6: unknown plugin `%s`", pluginConf.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load DHCPv4 plugins. Yes, duplicated code, there's not really much that
|
||||
// can be deduplicated here.
|
||||
if conf.Server4 != nil {
|
||||
for _, pluginConf := range conf.Server4.Plugins {
|
||||
if plugin, ok := RegisteredPlugins[pluginConf.Name]; ok {
|
||||
log.Printf("DHCPv4: loading plugin `%s`", pluginConf.Name)
|
||||
if plugin.Setup4 == nil {
|
||||
log.Warningf("DHCPv4: plugin `%s` has no setup function for DHCPv4", pluginConf.Name)
|
||||
continue
|
||||
}
|
||||
h4, err := plugin.Setup4(pluginConf.Args...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
} else if h4 == nil {
|
||||
return nil, nil, config.ConfigErrorFromString("no DHCPv4 handler for plugin %s", pluginConf.Name)
|
||||
}
|
||||
handlers4 = append(handlers4, h4)
|
||||
} else {
|
||||
return nil, nil, config.ConfigErrorFromString("DHCPv4: unknown plugin `%s`", pluginConf.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return handlers4, handlers6, nil
|
||||
}
|
||||
271
plugins/prefix/plugin.go
Normal file
271
plugins/prefix/plugin.go
Normal file
@@ -0,0 +1,271 @@
|
||||
// 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 prefix implements a plugin offering prefixes to clients requesting them
|
||||
// This plugin attributes prefixes to clients requesting them with IA_PREFIX requests.
|
||||
//
|
||||
// Arguments for the plugin configuration are as follows, in this order:
|
||||
// - prefix: The base prefix from which assigned prefixes are carved
|
||||
// - max: maximum size of the prefix delegated to clients. When a client requests a larger prefix
|
||||
// than this, this is the size of the offered prefix
|
||||
package prefix
|
||||
|
||||
// FIXME: various settings will be hardcoded (default size, minimum size, lease times) pending a
|
||||
// better configuration system
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bits-and-blooms/bitset"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
dhcpIana "github.com/insomniacslk/dhcp/iana"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/coredhcp/coredhcp/plugins/allocators"
|
||||
"github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/prefix")
|
||||
|
||||
// Plugin registers the prefix. Prefix delegation only exists for DHCPv6
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "prefix",
|
||||
Setup6: setupPrefix,
|
||||
}
|
||||
|
||||
const leaseDuration = 3600 * time.Second
|
||||
|
||||
func setupPrefix(args ...string) (handler.Handler6, error) {
|
||||
// - prefix: 2001:db8::/48 64
|
||||
if len(args) < 2 {
|
||||
return nil, errors.New("Need both a subnet and an allocation max size")
|
||||
}
|
||||
|
||||
_, prefix, err := net.ParseCIDR(args[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid pool subnet: %v", err)
|
||||
}
|
||||
|
||||
allocSize, err := strconv.Atoi(args[1])
|
||||
if err != nil || allocSize > 128 || allocSize < 0 {
|
||||
return nil, fmt.Errorf("Invalid prefix length: %v", err)
|
||||
}
|
||||
|
||||
// TODO: select allocators based on heuristics or user configuration
|
||||
alloc, err := bitmap.NewBitmapAllocator(*prefix, allocSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not initialize prefix allocator: %v", err)
|
||||
}
|
||||
|
||||
return (&Handler{
|
||||
Records: make(map[string][]lease),
|
||||
allocator: alloc,
|
||||
}).Handle, nil
|
||||
}
|
||||
|
||||
type lease struct {
|
||||
Prefix net.IPNet
|
||||
Expire time.Time
|
||||
}
|
||||
|
||||
// Handler holds state of allocations for the plugin
|
||||
type Handler struct {
|
||||
// Mutex here is the simplest implementation fit for purpose.
|
||||
// We can revisit for perf when we move lease management to separate plugins
|
||||
sync.Mutex
|
||||
// Records has a string'd []byte as key, because []byte can't be a key itself
|
||||
// Since it's not valid utf-8 we can't use any other string function though
|
||||
Records map[string][]lease
|
||||
allocator allocators.Allocator
|
||||
}
|
||||
|
||||
// samePrefix returns true if both prefixes are defined and equal
|
||||
// The empty prefix is equal to nothing, not even itself
|
||||
func samePrefix(a, b *net.IPNet) bool {
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
return a.IP.Equal(b.IP) && bytes.Equal(a.Mask, b.Mask)
|
||||
}
|
||||
|
||||
// recordKey computes the key for the Records array from the client ID
|
||||
func recordKey(d dhcpv6.DUID) string {
|
||||
return string(d.ToBytes())
|
||||
}
|
||||
|
||||
// Handle processes DHCPv6 packets for the prefix plugin for a given allocator/leaseset
|
||||
func (h *Handler) Handle(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
|
||||
msg, err := req.GetInnerMessage()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
client := msg.Options.ClientID()
|
||||
if client == nil {
|
||||
log.Error("Invalid packet received, no clientID")
|
||||
return nil, true
|
||||
}
|
||||
|
||||
// Each request IA_PD requires an IA_PD response
|
||||
for _, iapd := range msg.Options.IAPD() {
|
||||
if err != nil {
|
||||
log.Errorf("Malformed IAPD received: %v", err)
|
||||
resp.AddOption(&dhcpv6.OptStatusCode{StatusCode: dhcpIana.StatusMalformedQuery})
|
||||
return resp, true
|
||||
}
|
||||
|
||||
iapdResp := &dhcpv6.OptIAPD{
|
||||
IaId: iapd.IaId,
|
||||
}
|
||||
|
||||
// First figure out what prefixes the client wants
|
||||
hints := iapd.Options.Prefixes()
|
||||
if len(hints) == 0 {
|
||||
// If there are no IAPrefix hints, this is still a valid IA_PD request (just
|
||||
// unspecified) and we must attempt to allocate a prefix; so we include an empty hint
|
||||
// which is equivalent to no hint
|
||||
hints = []*dhcpv6.OptIAPrefix{{Prefix: &net.IPNet{}}}
|
||||
}
|
||||
|
||||
// Bitmap to track which requests are already satisfied or not
|
||||
satisfied := bitset.New(uint(len(hints)))
|
||||
|
||||
// A possible simple optimization here would be to be able to lock single map values
|
||||
// individually instead of the whole map, since we lock for some amount of time
|
||||
h.Lock()
|
||||
knownLeases := h.Records[recordKey(client)]
|
||||
// Bitmap to track which leases are already given in this exchange
|
||||
givenOut := bitset.New(uint(len(knownLeases)))
|
||||
|
||||
// This is, for now, a set of heuristics, to reconcile the requests (prefix hints asked
|
||||
// by the clients) with what's on offer (existing leases for this client, plus new blocks)
|
||||
|
||||
// Try to find leases that exactly match a hint, and extend them to satisfy the request
|
||||
// This is the safest heuristic, if the lease matches exactly we know we aren't missing
|
||||
// assigning it to a better candidate request
|
||||
for hintIdx, h := range hints {
|
||||
for leaseIdx := range knownLeases {
|
||||
if samePrefix(h.Prefix, &knownLeases[leaseIdx].Prefix) {
|
||||
expire := time.Now().Add(leaseDuration)
|
||||
if knownLeases[leaseIdx].Expire.Before(expire) {
|
||||
knownLeases[leaseIdx].Expire = expire
|
||||
}
|
||||
satisfied.Set(uint(hintIdx))
|
||||
givenOut.Set(uint(leaseIdx))
|
||||
addPrefix(iapdResp, knownLeases[leaseIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Then handle the empty hints, by giving out any remaining lease we
|
||||
// have already assigned to this client
|
||||
for hintIdx, h := range hints {
|
||||
if satisfied.Test(uint(hintIdx)) ||
|
||||
(h.Prefix != nil && !h.Prefix.IP.Equal(net.IPv6zero)) {
|
||||
continue
|
||||
}
|
||||
for leaseIdx, l := range knownLeases {
|
||||
if givenOut.Test(uint(leaseIdx)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If a length was requested, only give out prefixes of that length
|
||||
// This is a bad heuristic depending on the allocator behavior, to be improved
|
||||
if hintPrefixLen, _ := h.Prefix.Mask.Size(); hintPrefixLen != 0 {
|
||||
leasePrefixLen, _ := l.Prefix.Mask.Size()
|
||||
if hintPrefixLen != leasePrefixLen {
|
||||
continue
|
||||
}
|
||||
}
|
||||
expire := time.Now().Add(leaseDuration)
|
||||
if knownLeases[leaseIdx].Expire.Before(expire) {
|
||||
knownLeases[leaseIdx].Expire = expire
|
||||
}
|
||||
satisfied.Set(uint(hintIdx))
|
||||
givenOut.Set(uint(leaseIdx))
|
||||
addPrefix(iapdResp, knownLeases[leaseIdx])
|
||||
}
|
||||
}
|
||||
|
||||
// Now remains requests with a hint that we can't trivially satisfy, and possibly expired
|
||||
// leases that haven't been explicitly requested again.
|
||||
// A possible improvement here would be to try to widen existing leases, to satisfy wider
|
||||
// requests that contain an existing leases; and to try to break down existing leases into
|
||||
// smaller allocations, to satisfy requests for a subnet of an existing lease
|
||||
// We probably don't need such complex behavior (the vast majority of requests will come
|
||||
// with an empty, or length-only hint)
|
||||
|
||||
// Assign a new lease to satisfy the request
|
||||
var newLeases []lease
|
||||
for i, prefix := range hints {
|
||||
if satisfied.Test(uint(i)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if prefix.Prefix == nil {
|
||||
// XXX: replace usage of dhcp.OptIAPrefix with a better struct in this inner
|
||||
// function to avoid repeated nullpointer checks
|
||||
prefix.Prefix = &net.IPNet{}
|
||||
}
|
||||
allocated, err := h.allocator.Allocate(*prefix.Prefix)
|
||||
if err != nil {
|
||||
log.Debugf("Nothing allocated for hinted prefix %s", prefix)
|
||||
continue
|
||||
}
|
||||
l := lease{
|
||||
Expire: time.Now().Add(leaseDuration),
|
||||
Prefix: allocated,
|
||||
}
|
||||
|
||||
addPrefix(iapdResp, l)
|
||||
newLeases = append(knownLeases, l)
|
||||
log.Debugf("Allocated %s to %s (IAID: %x)", &allocated, client, iapd.IaId)
|
||||
}
|
||||
|
||||
if newLeases != nil {
|
||||
h.Records[recordKey(client)] = newLeases
|
||||
}
|
||||
h.Unlock()
|
||||
|
||||
if len(iapdResp.Options.Options) == 0 {
|
||||
log.Debugf("No valid prefix to return for IAID %x", iapd.IaId)
|
||||
iapdResp.Options.Add(&dhcpv6.OptStatusCode{
|
||||
StatusCode: dhcpIana.StatusNoPrefixAvail,
|
||||
})
|
||||
}
|
||||
|
||||
resp.AddOption(iapdResp)
|
||||
}
|
||||
|
||||
return resp, false
|
||||
}
|
||||
|
||||
func addPrefix(resp *dhcpv6.OptIAPD, l lease) {
|
||||
lifetime := time.Until(l.Expire)
|
||||
|
||||
resp.Options.Add(&dhcpv6.OptIAPrefix{
|
||||
PreferredLifetime: lifetime,
|
||||
ValidLifetime: lifetime,
|
||||
Prefix: dup(&l.Prefix),
|
||||
})
|
||||
}
|
||||
|
||||
func dup(src *net.IPNet) (dst *net.IPNet) {
|
||||
dst = &net.IPNet{
|
||||
IP: make(net.IP, net.IPv6len),
|
||||
Mask: make(net.IPMask, net.IPv6len),
|
||||
}
|
||||
copy(dst.IP, src.IP)
|
||||
copy(dst.Mask, src.Mask)
|
||||
return dst
|
||||
}
|
||||
93
plugins/prefix/plugin_test.go
Normal file
93
plugins/prefix/plugin_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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 prefix
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
dhcpIana "github.com/insomniacslk/dhcp/iana"
|
||||
)
|
||||
|
||||
func TestRoundTrip(t *testing.T) {
|
||||
reqIAID := [4]uint8{0x12, 0x34, 0x56, 0x78}
|
||||
|
||||
req, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.AddOption(dhcpv6.OptClientID(&dhcpv6.DUIDLL{
|
||||
HWType: dhcpIana.HWTypeEthernet,
|
||||
LinkLayerAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
|
||||
}))
|
||||
req.AddOption(&dhcpv6.OptIAPD{
|
||||
IaId: reqIAID,
|
||||
T1: 0,
|
||||
T2: 0,
|
||||
})
|
||||
|
||||
resp, err := dhcpv6.NewAdvertiseFromSolicit(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
handler, err := setupPrefix("2001:db8::/48", "64")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, final := handler(req, resp)
|
||||
if final {
|
||||
t.Log("Handler declared final")
|
||||
}
|
||||
t.Logf("%#v", result)
|
||||
|
||||
// Sanity checks on the response
|
||||
success := result.GetOption(dhcpv6.OptionStatusCode)
|
||||
var mo dhcpv6.MessageOptions
|
||||
if len(success) > 1 {
|
||||
t.Fatal("Got multiple StatusCode options")
|
||||
} else if len(success) == 0 { // Everything OK
|
||||
} else if err := mo.FromBytes(success[0].ToBytes()); err != nil || mo.Status().StatusCode != dhcpIana.StatusSuccess {
|
||||
t.Fatalf("Did not get a (implicit or explicit) success status code: %v", success)
|
||||
}
|
||||
|
||||
var iapd *dhcpv6.OptIAPD
|
||||
{
|
||||
// Check for IA_PD
|
||||
iapds := result.(*dhcpv6.Message).Options.IAPD()
|
||||
if len(iapds) != 1 {
|
||||
t.Fatal("Malformed response, expected exactly 1 IAPD")
|
||||
}
|
||||
iapd = iapds[0]
|
||||
}
|
||||
if iapd.IaId != reqIAID {
|
||||
t.Fatalf("IAID doesn't match: request %x, response: %x", iapd.IaId, reqIAID)
|
||||
}
|
||||
|
||||
// Check the status code
|
||||
if status := result.(*dhcpv6.Message).Options.Status(); status != nil && status.StatusCode != dhcpIana.StatusSuccess {
|
||||
t.Fatalf("Did not get a (implicit or explicit) success status code: %v", success)
|
||||
}
|
||||
|
||||
t.Logf("%#v", iapd)
|
||||
// Check IAPrefix within IAPD
|
||||
if len(iapd.Options.Prefixes()) != 1 {
|
||||
t.Fatalf("Response did not contain exactly one prefix in the IA_PD option (found %s)",
|
||||
iapd.Options.Prefixes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDup(t *testing.T) {
|
||||
_, prefix, err := net.ParseCIDR("2001:db8::/48")
|
||||
if err != nil {
|
||||
panic("bad cidr")
|
||||
}
|
||||
dupPrefix := dup(prefix)
|
||||
if !samePrefix(dupPrefix, prefix) {
|
||||
t.Fatalf("dup doesn't work: got %v expected %v", dupPrefix, prefix)
|
||||
}
|
||||
}
|
||||
193
plugins/range/plugin.go
Normal file
193
plugins/range/plugin.go
Normal file
@@ -0,0 +1,193 @@
|
||||
// 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 rangeplugin
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coredhcp/coredhcp/grpc_server/dhcpServer"
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/coredhcp/coredhcp/plugins/allocators"
|
||||
"github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/range")
|
||||
|
||||
// Plugin wraps plugin registration information
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "range",
|
||||
Setup4: setupRange,
|
||||
}
|
||||
|
||||
//Record holds an IP lease record
|
||||
type Record struct {
|
||||
IP net.IP
|
||||
Static bool
|
||||
Expires int
|
||||
Hostname string
|
||||
}
|
||||
|
||||
// PluginState is the data held by an instance of the range plugin
|
||||
type PluginState struct {
|
||||
// Rough lock for the whole plugin, we'll get better performance once we use leasestorage
|
||||
sync.Mutex
|
||||
// Recordsv4 holds a MAC -> IP address and lease time mapping
|
||||
Recordsv4 map[string]*Record
|
||||
LeaseTime time.Duration
|
||||
leasedb *sql.DB
|
||||
allocator allocators.Allocator
|
||||
}
|
||||
|
||||
var p PluginState
|
||||
|
||||
// Handler4 handles DHCPv4 packets for the range plugin
|
||||
func (p *PluginState) Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
record, ok := p.Recordsv4[req.ClientHWAddr.String()]
|
||||
hostname := req.HostName()
|
||||
if !ok {
|
||||
// Allocating new address since there isn't one allocated
|
||||
log.Printf("MAC address %s is new, leasing new IPv4 address", req.ClientHWAddr.String())
|
||||
var netIp net.IP
|
||||
var isStatic bool
|
||||
if addr := GetStaticIp(req.ClientHWAddr.String()); addr != "" {
|
||||
netIp = net.ParseIP(addr)
|
||||
isStatic = true
|
||||
} else {
|
||||
ip, err := p.allocator.Allocate(net.IPNet{})
|
||||
if err != nil {
|
||||
log.Errorf("Could not allocate IP for MAC %s: %v", req.ClientHWAddr.String(), err)
|
||||
return nil, true
|
||||
}
|
||||
netIp = ip.IP.To4()
|
||||
}
|
||||
rec := Record{
|
||||
IP: netIp,
|
||||
Static: isStatic,
|
||||
Expires: int(time.Now().Add(p.LeaseTime).Unix()),
|
||||
Hostname: hostname,
|
||||
}
|
||||
err := p.saveIPAddress(req.ClientHWAddr, &rec)
|
||||
if err != nil {
|
||||
log.Errorf("SaveIPAddress for MAC %s failed: %v", req.ClientHWAddr.String(), err)
|
||||
}
|
||||
p.Recordsv4[req.ClientHWAddr.String()] = &rec
|
||||
record = &rec
|
||||
} else {
|
||||
if addr := GetStaticIp(req.ClientHWAddr.String()); addr != "" {
|
||||
record.IP = net.ParseIP(addr)
|
||||
record.Static = true
|
||||
}
|
||||
// Ensure we extend the existing lease at least past when the one we're giving expires
|
||||
expiry := time.Unix(int64(record.Expires), 0)
|
||||
if expiry.Before(time.Now().Add(p.LeaseTime)) {
|
||||
record.Expires = int(time.Now().Add(p.LeaseTime).Round(time.Second).Unix())
|
||||
record.Hostname = hostname
|
||||
err := p.saveIPAddress(req.ClientHWAddr, record)
|
||||
if err != nil {
|
||||
log.Errorf("Could not persist lease for MAC %s: %v", req.ClientHWAddr.String(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.YourIPAddr = record.IP
|
||||
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second)))
|
||||
log.Printf("found IP address %s for MAC %s", record.IP, req.ClientHWAddr.String())
|
||||
return resp, false
|
||||
}
|
||||
|
||||
func GetRecord(clientHWAddr string) *Record {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
return p.Recordsv4[clientHWAddr]
|
||||
}
|
||||
|
||||
func GetDhcpInfo() (*dhcpServer.DhcpInfo, error) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
var dhcpInfo dhcpServer.DhcpInfo
|
||||
for mac, record := range p.Recordsv4 {
|
||||
if record.Static { continue }
|
||||
endTime := time.Unix(int64(record.Expires), 0)
|
||||
startTime := endTime.Add(-p.LeaseTime)
|
||||
dhcpInfo.UeInfo = append(dhcpInfo.UeInfo, &dhcpServer.UeInfo{
|
||||
Ip: record.IP.String(),
|
||||
Mac: mac,
|
||||
Hostname: record.Hostname,
|
||||
StartTime: startTime.Format(time.DateTime),
|
||||
EndTime: endTime.Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
return &dhcpInfo, nil
|
||||
}
|
||||
|
||||
func setupRange(args ...string) (handler.Handler4, error) {
|
||||
var err error
|
||||
|
||||
if len(args) < 4 {
|
||||
return nil, fmt.Errorf("invalid number of arguments, want: 4 (file name, start IP, end IP, lease time), got: %d", len(args))
|
||||
}
|
||||
filename := args[0]
|
||||
if filename == "" {
|
||||
return nil, errors.New("file name cannot be empty")
|
||||
}
|
||||
ipRangeStart := net.ParseIP(args[1])
|
||||
if ipRangeStart.To4() == nil {
|
||||
return nil, fmt.Errorf("invalid IPv4 address: %v", args[1])
|
||||
}
|
||||
ipRangeEnd := net.ParseIP(args[2])
|
||||
if ipRangeEnd.To4() == nil {
|
||||
return nil, fmt.Errorf("invalid IPv4 address: %v", args[2])
|
||||
}
|
||||
if binary.BigEndian.Uint32(ipRangeStart.To4()) >= binary.BigEndian.Uint32(ipRangeEnd.To4()) {
|
||||
return nil, errors.New("start of IP range has to be lower than the end of an IP range")
|
||||
}
|
||||
|
||||
p.allocator, err = bitmap.NewIPv4Allocator(ipRangeStart, ipRangeEnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create an allocator: %w", err)
|
||||
}
|
||||
|
||||
p.LeaseTime, err = time.ParseDuration(args[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid lease duration: %v", args[3])
|
||||
}
|
||||
|
||||
if err := p.registerBackingDB(filename); err != nil {
|
||||
return nil, fmt.Errorf("could not setup lease storage: %w", err)
|
||||
}
|
||||
p.Recordsv4, err = loadRecords(p.leasedb)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load records from file: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Loaded %d DHCPv4 leases from %s", len(p.Recordsv4), filename)
|
||||
|
||||
for _, v := range p.Recordsv4 {
|
||||
if v.Static {
|
||||
continue
|
||||
}
|
||||
ip, err := p.allocator.Allocate(net.IPNet{IP: v.IP})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to re-allocate leased ip %v: %v", v.IP.String(), err)
|
||||
}
|
||||
if ip.IP.String() != v.IP.String() {
|
||||
return nil, fmt.Errorf("allocator did not re-allocate requested leased ip %v: %v", v.IP.String(), ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
importStaticIpFile()
|
||||
|
||||
return p.Handler4, nil
|
||||
}
|
||||
169
plugins/range/plugin.go.new
Normal file
169
plugins/range/plugin.go.new
Normal file
@@ -0,0 +1,169 @@
|
||||
// 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 rangeplugin
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/coredhcp/coredhcp/plugins/allocators"
|
||||
"github.com/coredhcp/coredhcp/plugins/allocators/bitmap"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/range")
|
||||
|
||||
// Plugin wraps plugin registration information
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "range",
|
||||
Setup4: setupRange,
|
||||
}
|
||||
|
||||
//Record holds an IP lease record
|
||||
type Record struct {
|
||||
IP net.IP
|
||||
expires int
|
||||
hostname string
|
||||
}
|
||||
|
||||
// PluginState is the data held by an instance of the range plugin
|
||||
type PluginState struct {
|
||||
// Rough lock for the whole plugin, we'll get better performance once we use leasestorage
|
||||
sync.Mutex
|
||||
// Recordsv4 holds a MAC -> IP address and lease time mapping
|
||||
Recordsv4 map[string]*Record
|
||||
LeaseTime time.Duration
|
||||
leasedb *sql.DB
|
||||
allocator allocators.Allocator
|
||||
}
|
||||
|
||||
var p PluginState
|
||||
|
||||
// Handler4 handles DHCPv4 packets for the range plugin
|
||||
func (p *PluginState) Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
|
||||
if ip := GetStaticIp(req.ClientHWAddr.String()); ip != "" {
|
||||
resp.YourIPAddr = net.ParseIP(ip)
|
||||
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second)))
|
||||
log.Printf("found static IP address %s for MAC %s", ip, req.ClientHWAddr.String())
|
||||
return resp, false
|
||||
}
|
||||
|
||||
record, ok := p.Recordsv4[req.ClientHWAddr.String()]
|
||||
hostname := req.HostName()
|
||||
if !ok {
|
||||
// Allocating new address since there isn't one allocated
|
||||
log.Printf("MAC address %s is new, leasing new IPv4 address", req.ClientHWAddr.String())
|
||||
ip, err := p.allocator.Allocate(net.IPNet{})
|
||||
if err != nil {
|
||||
log.Errorf("Could not allocate IP for MAC %s: %v", req.ClientHWAddr.String(), err)
|
||||
return nil, true
|
||||
}
|
||||
rec := Record{
|
||||
IP: ip.IP.To4(),
|
||||
expires: int(time.Now().Add(p.LeaseTime).Unix()),
|
||||
hostname: hostname,
|
||||
}
|
||||
err = p.saveIPAddress(req.ClientHWAddr, &rec)
|
||||
if err != nil {
|
||||
log.Errorf("SaveIPAddress for MAC %s failed: %v", req.ClientHWAddr.String(), err)
|
||||
}
|
||||
p.Recordsv4[req.ClientHWAddr.String()] = &rec
|
||||
record = &rec
|
||||
} else {
|
||||
// Ensure we extend the existing lease at least past when the one we're giving expires
|
||||
expiry := time.Unix(int64(record.expires), 0)
|
||||
if expiry.Before(time.Now().Add(p.LeaseTime)) {
|
||||
record.expires = int(time.Now().Add(p.LeaseTime).Round(time.Second).Unix())
|
||||
record.hostname = hostname
|
||||
err := p.saveIPAddress(req.ClientHWAddr, record)
|
||||
if err != nil {
|
||||
log.Errorf("Could not persist lease for MAC %s: %v", req.ClientHWAddr.String(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
resp.YourIPAddr = record.IP
|
||||
resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second)))
|
||||
log.Printf("found IP address %s for MAC %s", record.IP, req.ClientHWAddr.String())
|
||||
return resp, false
|
||||
}
|
||||
|
||||
func GetRecord(clientHWAddr string) (string, string) {
|
||||
p.Lock()
|
||||
defer p.Unlock()
|
||||
record := p.Recordsv4[clientHWAddr]
|
||||
if record != nil {
|
||||
return record.IP.String(), record.hostname
|
||||
} else {
|
||||
return "-", "-"
|
||||
}
|
||||
}
|
||||
|
||||
func setupRange(args ...string) (handler.Handler4, error) {
|
||||
var err error
|
||||
|
||||
if len(args) < 4 {
|
||||
return nil, fmt.Errorf("invalid number of arguments, want: 4 (file name, start IP, end IP, lease time), got: %d", len(args))
|
||||
}
|
||||
filename := args[0]
|
||||
if filename == "" {
|
||||
return nil, errors.New("file name cannot be empty")
|
||||
}
|
||||
ipRangeStart := net.ParseIP(args[1])
|
||||
if ipRangeStart.To4() == nil {
|
||||
return nil, fmt.Errorf("invalid IPv4 address: %v", args[1])
|
||||
}
|
||||
ipRangeEnd := net.ParseIP(args[2])
|
||||
if ipRangeEnd.To4() == nil {
|
||||
return nil, fmt.Errorf("invalid IPv4 address: %v", args[2])
|
||||
}
|
||||
if binary.BigEndian.Uint32(ipRangeStart.To4()) >= binary.BigEndian.Uint32(ipRangeEnd.To4()) {
|
||||
return nil, errors.New("start of IP range has to be lower than the end of an IP range")
|
||||
}
|
||||
|
||||
p.allocator, err = bitmap.NewIPv4Allocator(ipRangeStart, ipRangeEnd)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create an allocator: %w", err)
|
||||
}
|
||||
|
||||
p.LeaseTime, err = time.ParseDuration(args[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid lease duration: %v", args[3])
|
||||
}
|
||||
|
||||
if err := p.registerBackingDB(filename); err != nil {
|
||||
return nil, fmt.Errorf("could not setup lease storage: %w", err)
|
||||
}
|
||||
p.Recordsv4, err = loadRecords(p.leasedb)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not load records from file: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Loaded %d DHCPv4 leases from %s", len(p.Recordsv4), filename)
|
||||
|
||||
for _, v := range p.Recordsv4 {
|
||||
ip, err := p.allocator.Allocate(net.IPNet{IP: v.IP})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to re-allocate leased ip %v: %v", v.IP.String(), err)
|
||||
}
|
||||
if ip.IP.String() != v.IP.String() {
|
||||
return nil, fmt.Errorf("allocator did not re-allocate requested leased ip %v: %v", v.IP.String(), ip.String())
|
||||
}
|
||||
}
|
||||
|
||||
importStaticIpFile()
|
||||
|
||||
return p.Handler4, nil
|
||||
}
|
||||
50
plugins/range/static_ip.go
Normal file
50
plugins/range/static_ip.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package rangeplugin
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
const staticIpFile string = "./static_ip.csv"
|
||||
|
||||
var staticIpPool map[string]string // mac as key
|
||||
var macPool map[string]string // ip as key
|
||||
|
||||
func importStaticIpFile() {
|
||||
fs, err := os.Open(staticIpFile)
|
||||
if err != nil {
|
||||
//fmt.Println(err)
|
||||
return
|
||||
}
|
||||
defer fs.Close()
|
||||
|
||||
staticIpPool = make(map[string]string)
|
||||
macPool = make(map[string]string)
|
||||
|
||||
r := csv.NewReader(fs)
|
||||
for {
|
||||
row, err := r.Read()
|
||||
if err != nil && err != io.EOF {
|
||||
fmt.Printf("Can not read, err is %+v", err)
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
//fmt.Println(row)
|
||||
if len(row) > 1 {
|
||||
staticIpPool[row[0]] = row[1]
|
||||
macPool[row[1]] = row[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetStaticIp(mac string) string {
|
||||
return staticIpPool[mac]
|
||||
}
|
||||
|
||||
func IsStaticIp(ip string) bool {
|
||||
_, ok := macPool[ip]
|
||||
return ok
|
||||
}
|
||||
93
plugins/range/storage.go
Normal file
93
plugins/range/storage.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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 rangeplugin
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
func loadDB(path string) (*sql.DB, error) {
|
||||
db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s", path))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open database (%T): %w", err, err)
|
||||
}
|
||||
if _, err := db.Exec("create table if not exists leases4 (mac string not null, ip string not null, static bool, expiry int, hostname string not null, primary key (mac, ip))"); err != nil {
|
||||
return nil, fmt.Errorf("table creation failed: %w", err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// loadRecords loads the DHCPv6/v4 Records global map with records stored on
|
||||
// the specified file. The records have to be one per line, a mac address and an
|
||||
// IP address.
|
||||
func loadRecords(db *sql.DB) (map[string]*Record, error) {
|
||||
rows, err := db.Query("select mac, ip, static, expiry, hostname from leases4")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query leases database: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var (
|
||||
mac, ip, hostname string
|
||||
static bool
|
||||
expiry int
|
||||
records = make(map[string]*Record)
|
||||
)
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&mac, &ip, &static, &expiry, &hostname); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan row: %w", err)
|
||||
}
|
||||
hwaddr, err := net.ParseMAC(mac)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed hardware address: %s", mac)
|
||||
}
|
||||
ipaddr := net.ParseIP(ip)
|
||||
if ipaddr.To4() == nil {
|
||||
return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr)
|
||||
}
|
||||
records[hwaddr.String()] = &Record{IP: ipaddr, Static: static, Expires: expiry, Hostname: hostname}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed lease database row scanning: %w", err)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// saveIPAddress writes out a lease to storage
|
||||
func (p *PluginState) saveIPAddress(mac net.HardwareAddr, record *Record) error {
|
||||
stmt, err := p.leasedb.Prepare(`insert or replace into leases4(mac, ip, static, expiry, hostname) values (?, ?, ?, ?, ?)`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("statement preparation failed: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.Exec(
|
||||
mac.String(),
|
||||
record.IP.String(),
|
||||
record.Static,
|
||||
record.Expires,
|
||||
record.Hostname,
|
||||
); err != nil {
|
||||
return fmt.Errorf("record insert/update failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerBackingDB installs a database connection string as the backing store for leases
|
||||
func (p *PluginState) registerBackingDB(filename string) error {
|
||||
if p.leasedb != nil {
|
||||
return errors.New("cannot swap out a lease database while running")
|
||||
}
|
||||
// We never close this, but that's ok because plugins are never stopped/unregistered
|
||||
newLeaseDB, err := loadDB(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open lease database %s: %w", filename, err)
|
||||
}
|
||||
p.leasedb = newLeaseDB
|
||||
return nil
|
||||
}
|
||||
99
plugins/range/storage_test.go
Normal file
99
plugins/range/storage_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// 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 rangeplugin
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testDBSetup() (*sql.DB, error) {
|
||||
db, err := loadDB(":memory:")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, record := range records {
|
||||
stmt, err := db.Prepare("insert into leases4(mac, ip, expiry, hostname) values (?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare insert statement: %w", err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
if _, err := stmt.Exec(record.mac, record.ip.IP.String(), record.ip.expires, record.ip.hostname); err != nil {
|
||||
return nil, fmt.Errorf("failed to insert record into test db: %w", err)
|
||||
}
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
var expire = int(time.Date(2000, 01, 01, 00, 00, 00, 00, time.UTC).Unix())
|
||||
var records = []struct {
|
||||
mac string
|
||||
ip *Record
|
||||
}{
|
||||
{"02:00:00:00:00:00", &Record{IP: net.IPv4(10, 0, 0, 0), expires: expire, hostname: "zero"}},
|
||||
{"02:00:00:00:00:01", &Record{IP: net.IPv4(10, 0, 0, 1), expires: expire, hostname: "one"}},
|
||||
{"02:00:00:00:00:02", &Record{IP: net.IPv4(10, 0, 0, 2), expires: expire, hostname: "two"}},
|
||||
{"02:00:00:00:00:03", &Record{IP: net.IPv4(10, 0, 0, 3), expires: expire, hostname: "three"}},
|
||||
{"02:00:00:00:00:04", &Record{IP: net.IPv4(10, 0, 0, 4), expires: expire, hostname: "four"}},
|
||||
{"02:00:00:00:00:05", &Record{IP: net.IPv4(10, 0, 0, 5), expires: expire, hostname: "five"}},
|
||||
}
|
||||
|
||||
func TestLoadRecords(t *testing.T) {
|
||||
db, err := testDBSetup()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to set up test DB: %v", err)
|
||||
}
|
||||
|
||||
parsedRec, err := loadRecords(db)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load records from file: %v", err)
|
||||
}
|
||||
|
||||
mapRec := make(map[string]*Record)
|
||||
for _, rec := range records {
|
||||
var (
|
||||
ip, mac, hostname string
|
||||
expiry int
|
||||
)
|
||||
if err := db.QueryRow("select mac, ip, expiry, hostname from leases4 where mac = ?", rec.mac).Scan(&mac, &ip, &expiry, &hostname); err != nil {
|
||||
t.Fatalf("record not found for mac=%s: %v", rec.mac, err)
|
||||
}
|
||||
mapRec[mac] = &Record{IP: net.ParseIP(ip), expires: expiry, hostname: hostname}
|
||||
}
|
||||
|
||||
assert.Equal(t, mapRec, parsedRec, "Loaded records differ from what's in the DB")
|
||||
}
|
||||
|
||||
func TestWriteRecords(t *testing.T) {
|
||||
pl := PluginState{}
|
||||
if err := pl.registerBackingDB(":memory:"); err != nil {
|
||||
t.Fatalf("Could not setup file")
|
||||
}
|
||||
|
||||
mapRec := make(map[string]*Record)
|
||||
for _, rec := range records {
|
||||
hwaddr, err := net.ParseMAC(rec.mac)
|
||||
if err != nil {
|
||||
// bug in testdata
|
||||
panic(err)
|
||||
}
|
||||
if err := pl.saveIPAddress(hwaddr, rec.ip); err != nil {
|
||||
t.Errorf("Failed to save ip for %s: %v", hwaddr, err)
|
||||
}
|
||||
mapRec[hwaddr.String()] = &Record{IP: rec.ip.IP, expires: rec.ip.expires, hostname: rec.ip.hostname}
|
||||
}
|
||||
|
||||
parsedRec, err := loadRecords(pl.leasedb)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, mapRec, parsedRec, "Loaded records differ from what's in the DB")
|
||||
}
|
||||
49
plugins/router/plugin.go
Normal file
49
plugins/router/plugin.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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 router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/router")
|
||||
|
||||
// Plugin wraps plugin registration information
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "router",
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
var (
|
||||
routers []net.IP
|
||||
)
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
log.Printf("Loaded plugin for DHCPv4.")
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("need at least one router IP address")
|
||||
}
|
||||
for _, arg := range args {
|
||||
router := net.ParseIP(arg)
|
||||
if router.To4() == nil {
|
||||
return Handler4, errors.New("expected an router IP address, got: " + arg)
|
||||
}
|
||||
routers = append(routers, router)
|
||||
}
|
||||
log.Infof("loaded %d router IP addresses.", len(routers))
|
||||
return Handler4, nil
|
||||
}
|
||||
|
||||
//Handler4 handles DHCPv4 packets for the router plugin
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
resp.Options.Update(dhcpv4.OptRouter(routers...))
|
||||
return resp, false
|
||||
}
|
||||
77
plugins/searchdomains/plugin.go
Normal file
77
plugins/searchdomains/plugin.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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 searchdomains
|
||||
|
||||
// This is an searchdomains plugin that adds default DNS search domains.
|
||||
|
||||
import (
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/insomniacslk/dhcp/rfc1035label"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/searchdomains")
|
||||
|
||||
// Plugin wraps the default DNS search domain options.
|
||||
// Note that importing the plugin is not enough to use it: you have to
|
||||
// explicitly specify the intention to use it in the `config.yml` file, in the
|
||||
// plugins section. For searchdomains:
|
||||
//
|
||||
// server6:
|
||||
// listen: '[::]547'
|
||||
// - searchdomains: domain.a domain.b
|
||||
// - server_id: LL aa:bb:cc:dd:ee:ff
|
||||
// - file: "leases.txt"
|
||||
//
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "searchdomains",
|
||||
Setup6: setup6,
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
// These are the DNS search domains that are set by the plugin.
|
||||
// Note that DHCPv4 and DHCPv6 options are totally independent.
|
||||
// If you need the same settings for both, you'll need to configure
|
||||
// this plugin once for the v4 and once for the v6 server.
|
||||
var v4SearchList []string
|
||||
var v6SearchList []string
|
||||
|
||||
// copySlice creates a new copy of a string slice in memory.
|
||||
// This helps to ensure that downstream plugins can't corrupt
|
||||
// this plugin's configuration
|
||||
func copySlice(original []string) []string {
|
||||
copied := make([]string, len(original))
|
||||
copy(copied, original)
|
||||
return copied
|
||||
}
|
||||
|
||||
func setup6(args ...string) (handler.Handler6, error) {
|
||||
v6SearchList = args
|
||||
log.Printf("Registered domain search list (DHCPv6) %s", v6SearchList)
|
||||
return domainSearchListHandler6, nil
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
v4SearchList = args
|
||||
log.Printf("Registered domain search list (DHCPv4) %s", v4SearchList)
|
||||
return domainSearchListHandler4, nil
|
||||
}
|
||||
|
||||
func domainSearchListHandler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
|
||||
resp.UpdateOption(dhcpv6.OptDomainSearchList(&rfc1035label.Labels{
|
||||
Labels: copySlice(v6SearchList),
|
||||
}))
|
||||
return resp, false
|
||||
}
|
||||
|
||||
func domainSearchListHandler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
resp.UpdateOption(dhcpv4.OptDomainSearch(&rfc1035label.Labels{
|
||||
Labels: copySlice(v4SearchList),
|
||||
}))
|
||||
return resp, false
|
||||
}
|
||||
96
plugins/searchdomains/plugin_test.go
Normal file
96
plugins/searchdomains/plugin_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// 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 searchdomains
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddDomains6(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Search domains we will expect the DHCP server to assign
|
||||
searchDomains := []string{"domain.a", "domain.b"}
|
||||
|
||||
// Init plugin
|
||||
handler6, err := Plugin.Setup6(searchDomains...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fake request
|
||||
req, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.MessageType = dhcpv6.MessageTypeRequest
|
||||
req.AddOption(dhcpv6.OptRequestedOption(dhcpv6.OptionDNSRecursiveNameServer))
|
||||
|
||||
// Fake response input
|
||||
stub, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stub.MessageType = dhcpv6.MessageTypeReply
|
||||
|
||||
// Call plugin
|
||||
resp, stop := handler6(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
|
||||
searchLabels := resp.(*dhcpv6.Message).Options.DomainSearchList().Labels
|
||||
assert.Equal(searchDomains, searchLabels)
|
||||
}
|
||||
|
||||
func TestAddDomains4(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
// Search domains we will expect the DHCP server to assign
|
||||
// NOTE: these domains should be different from the v6 test domains;
|
||||
// this tests that we haven't accidentally set the v6 domains in the
|
||||
// v4 plugin handler or vice versa.
|
||||
searchDomains := []string{"domain.b", "domain.c"}
|
||||
|
||||
// Init plugin
|
||||
handler4, err := Plugin.Setup4(searchDomains...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fake request
|
||||
req, err := dhcpv4.NewDiscovery(net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Fake response input
|
||||
stub, err := dhcpv4.NewReplyFromRequest(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Call plugin
|
||||
resp, stop := handler4(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return a message")
|
||||
}
|
||||
if stop {
|
||||
t.Error("plugin interrupted processing")
|
||||
}
|
||||
|
||||
searchLabels := resp.DomainSearch().Labels
|
||||
assert.Equal(searchDomains, searchLabels)
|
||||
|
||||
}
|
||||
155
plugins/serverid/plugin.go
Normal file
155
plugins/serverid/plugin.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// 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 serverid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
"github.com/insomniacslk/dhcp/iana"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/server_id")
|
||||
|
||||
// Plugin wraps plugin registration information
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "server_id",
|
||||
Setup6: setup6,
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
// v6ServerID is the DUID of the v6 server
|
||||
var (
|
||||
v6ServerID dhcpv6.DUID
|
||||
v4ServerID net.IP
|
||||
)
|
||||
|
||||
// Handler6 handles DHCPv6 packets for the server_id plugin.
|
||||
func Handler6(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
|
||||
if v6ServerID == nil {
|
||||
log.Fatal("BUG: Plugin is running uninitialized!")
|
||||
return nil, true
|
||||
}
|
||||
|
||||
msg, err := req.GetInnerMessage()
|
||||
if err != nil {
|
||||
// BUG: this should already have failed in the main handler. Abort
|
||||
log.Error(err)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
if sid := msg.Options.ServerID(); sid != nil {
|
||||
// RFC8415 §16.{2,5,7}
|
||||
// These message types MUST be discarded if they contain *any* ServerID option
|
||||
if msg.MessageType == dhcpv6.MessageTypeSolicit ||
|
||||
msg.MessageType == dhcpv6.MessageTypeConfirm ||
|
||||
msg.MessageType == dhcpv6.MessageTypeRebind {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
// Approximately all others MUST be discarded if the ServerID doesn't match
|
||||
if !sid.Equal(v6ServerID) {
|
||||
log.Infof("requested server ID does not match this server's ID. Got %v, want %v", sid, v6ServerID)
|
||||
return nil, true
|
||||
}
|
||||
} else if msg.MessageType == dhcpv6.MessageTypeRequest ||
|
||||
msg.MessageType == dhcpv6.MessageTypeRenew ||
|
||||
msg.MessageType == dhcpv6.MessageTypeDecline ||
|
||||
msg.MessageType == dhcpv6.MessageTypeRelease {
|
||||
// RFC8415 §16.{6,8,10,11}
|
||||
// These message types MUST be discarded if they *don't* contain a ServerID option
|
||||
return nil, true
|
||||
}
|
||||
dhcpv6.WithServerID(v6ServerID)(resp)
|
||||
return resp, false
|
||||
}
|
||||
|
||||
// Handler4 handles DHCPv4 packets for the server_id plugin.
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
if v4ServerID == nil {
|
||||
log.Fatal("BUG: Plugin is running uninitialized!")
|
||||
return nil, true
|
||||
}
|
||||
if req.OpCode != dhcpv4.OpcodeBootRequest {
|
||||
log.Warningf("not a BootRequest, ignoring")
|
||||
return resp, false
|
||||
}
|
||||
if req.ServerIPAddr != nil &&
|
||||
!req.ServerIPAddr.Equal(net.IPv4zero) &&
|
||||
!req.ServerIPAddr.Equal(v4ServerID) {
|
||||
// This request is not for us, drop it.
|
||||
log.Infof("requested server ID does not match this server's ID. Got %v, want %v", req.ServerIPAddr, v4ServerID)
|
||||
return nil, true
|
||||
}
|
||||
resp.ServerIPAddr = make(net.IP, net.IPv4len)
|
||||
copy(resp.ServerIPAddr[:], v4ServerID)
|
||||
resp.UpdateOption(dhcpv4.OptServerIdentifier(v4ServerID))
|
||||
return resp, false
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
log.Printf("loading `server_id` plugin for DHCPv4 with args: %v", args)
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("need an argument")
|
||||
}
|
||||
serverID := net.ParseIP(args[0])
|
||||
if serverID == nil {
|
||||
return nil, errors.New("invalid or empty IP address")
|
||||
}
|
||||
if serverID.To4() == nil {
|
||||
return nil, errors.New("not a valid IPv4 address")
|
||||
}
|
||||
v4ServerID = serverID.To4()
|
||||
return Handler4, nil
|
||||
}
|
||||
|
||||
func setup6(args ...string) (handler.Handler6, error) {
|
||||
log.Printf("loading `server_id` plugin for DHCPv6 with args: %v", args)
|
||||
if len(args) < 2 {
|
||||
return nil, errors.New("need a DUID type and value")
|
||||
}
|
||||
duidType := args[0]
|
||||
if duidType == "" {
|
||||
return nil, errors.New("got empty DUID type")
|
||||
}
|
||||
duidValue := args[1]
|
||||
if duidValue == "" {
|
||||
return nil, errors.New("got empty DUID value")
|
||||
}
|
||||
duidType = strings.ToLower(duidType)
|
||||
hwaddr, err := net.ParseMAC(duidValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch duidType {
|
||||
case "ll", "duid-ll", "duid_ll":
|
||||
v6ServerID = &dhcpv6.DUIDLL{
|
||||
// sorry, only ethernet for now
|
||||
HWType: iana.HWTypeEthernet,
|
||||
LinkLayerAddr: hwaddr,
|
||||
}
|
||||
case "llt", "duid-llt", "duid_llt":
|
||||
v6ServerID = &dhcpv6.DUIDLLT{
|
||||
// sorry, zero-time for now
|
||||
Time: 0,
|
||||
// sorry, only ethernet for now
|
||||
HWType: iana.HWTypeEthernet,
|
||||
LinkLayerAddr: hwaddr,
|
||||
}
|
||||
case "en", "uuid":
|
||||
return nil, errors.New("EN/UUID DUID type not supported yet")
|
||||
default:
|
||||
return nil, errors.New("Opaque DUID type not supported yet")
|
||||
}
|
||||
log.Printf("using %s %s", duidType, duidValue)
|
||||
|
||||
return Handler6, nil
|
||||
}
|
||||
127
plugins/serverid/plugin_test.go
Normal file
127
plugins/serverid/plugin_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
// 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 serverid
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
)
|
||||
|
||||
func makeTestDUID(uuid string) dhcpv6.DUID {
|
||||
var uuidb [16]byte
|
||||
copy(uuidb[:], uuid)
|
||||
return &dhcpv6.DUIDUUID{
|
||||
UUID: uuidb,
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectBadServerIDV6(t *testing.T) {
|
||||
req, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v6ServerID = makeTestDUID("0000000000000000")
|
||||
|
||||
req.MessageType = dhcpv6.MessageTypeRenew
|
||||
dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req)
|
||||
dhcpv6.WithServerID(makeTestDUID("0000000000000001"))(req)
|
||||
|
||||
stub, err := dhcpv6.NewReplyFromMessage(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, stop := Handler6(req, stub)
|
||||
if resp != nil {
|
||||
t.Error("server_id is sending a response message to a request with mismatched ServerID")
|
||||
}
|
||||
if !stop {
|
||||
t.Error("server_id did not interrupt processing on a request with mismatched ServerID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectUnexpectedServerIDV6(t *testing.T) {
|
||||
req, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v6ServerID = makeTestDUID("0000000000000000")
|
||||
|
||||
req.MessageType = dhcpv6.MessageTypeSolicit
|
||||
dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req)
|
||||
dhcpv6.WithServerID(makeTestDUID("0000000000000000"))(req)
|
||||
|
||||
stub, err := dhcpv6.NewAdvertiseFromSolicit(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, stop := Handler6(req, stub)
|
||||
if resp != nil {
|
||||
t.Error("server_id is sending a response message to a solicit with a ServerID")
|
||||
}
|
||||
if !stop {
|
||||
t.Error("server_id did not interrupt processing on a solicit with a ServerID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddServerIDV6(t *testing.T) {
|
||||
req, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v6ServerID = makeTestDUID("0000000000000000")
|
||||
|
||||
req.MessageType = dhcpv6.MessageTypeRebind
|
||||
dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req)
|
||||
|
||||
stub, err := dhcpv6.NewReplyFromMessage(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, _ := Handler6(req, stub)
|
||||
if resp == nil {
|
||||
t.Fatal("plugin did not return an answer")
|
||||
}
|
||||
|
||||
if opt := resp.(*dhcpv6.Message).Options.ServerID(); opt == nil {
|
||||
t.Fatal("plugin did not add a ServerID option")
|
||||
} else if !opt.Equal(v6ServerID) {
|
||||
t.Fatalf("Got unexpected DUID: expected %v, got %v", v6ServerID, opt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectInnerMessageServerID(t *testing.T) {
|
||||
req, err := dhcpv6.NewMessage()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v6ServerID = makeTestDUID("0000000000000000")
|
||||
|
||||
req.MessageType = dhcpv6.MessageTypeSolicit
|
||||
dhcpv6.WithClientID(makeTestDUID("1000000000000000"))(req)
|
||||
dhcpv6.WithServerID(makeTestDUID("0000000000000000"))(req)
|
||||
|
||||
stub, err := dhcpv6.NewAdvertiseFromSolicit(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
relayedRequest, err := dhcpv6.EncapsulateRelay(req, dhcpv6.MessageTypeRelayForward, net.IPv6loopback, net.IPv6loopback)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp, stop := Handler6(relayedRequest, stub)
|
||||
if resp != nil {
|
||||
t.Error("server_id is sending a response message to a relayed solicit with a ServerID")
|
||||
}
|
||||
if !stop {
|
||||
t.Error("server_id did not interrupt processing on a relayed solicit with a ServerID")
|
||||
}
|
||||
}
|
||||
89
plugins/sleep/plugin.go
Normal file
89
plugins/sleep/plugin.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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 sleep
|
||||
|
||||
// This plugin introduces a delay in the DHCP response.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
"github.com/insomniacslk/dhcp/dhcpv6"
|
||||
)
|
||||
|
||||
var (
|
||||
pluginName = "sleep"
|
||||
log = logger.GetLogger("plugins/" + pluginName)
|
||||
)
|
||||
|
||||
// Example configuration of the `sleep` plugin:
|
||||
//
|
||||
// server4:
|
||||
// plugins:
|
||||
// - sleep 300ms
|
||||
// - file: "leases4.txt"
|
||||
//
|
||||
// server6:
|
||||
// plugins:
|
||||
// - sleep 1s
|
||||
// - file: "leases6.txt"
|
||||
//
|
||||
// For the duration format, see the documentation of `time.ParseDuration`,
|
||||
// https://golang.org/pkg/time/#ParseDuration .
|
||||
|
||||
// Plugin contains the `sleep` plugin data.
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: pluginName,
|
||||
Setup6: setup6,
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
func setup6(args ...string) (handler.Handler6, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, fmt.Errorf("want exactly one argument, got %d", len(args))
|
||||
}
|
||||
delay, err := time.ParseDuration(args[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse duration: %w", err)
|
||||
}
|
||||
log.Printf("loaded plugin for DHCPv6.")
|
||||
return makeSleepHandler6(delay), nil
|
||||
}
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
if len(args) != 1 {
|
||||
return nil, fmt.Errorf("want exactly one argument, got %d", len(args))
|
||||
}
|
||||
delay, err := time.ParseDuration(args[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse duration: %w", err)
|
||||
}
|
||||
log.Printf("loaded plugin for DHCPv4.")
|
||||
return makeSleepHandler4(delay), nil
|
||||
}
|
||||
|
||||
func makeSleepHandler6(delay time.Duration) handler.Handler6 {
|
||||
return func(req, resp dhcpv6.DHCPv6) (dhcpv6.DHCPv6, bool) {
|
||||
log.Printf("introducing delay of %s in response", delay)
|
||||
// return the unmodified response, and instruct coredhcp to continue to
|
||||
// the next plugin.
|
||||
time.Sleep(delay)
|
||||
return resp, false
|
||||
}
|
||||
}
|
||||
|
||||
func makeSleepHandler4(delay time.Duration) handler.Handler4 {
|
||||
return func(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
log.Printf("introducing delay of %s in response", delay)
|
||||
// return the unmodified response, and instruct coredhcp to continue to
|
||||
// the next plugin.
|
||||
time.Sleep(delay)
|
||||
return resp, false
|
||||
}
|
||||
}
|
||||
73
plugins/staticroute/plugin.go
Normal file
73
plugins/staticroute/plugin.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// 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 staticroute
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/coredhcp/coredhcp/handler"
|
||||
"github.com/coredhcp/coredhcp/logger"
|
||||
"github.com/coredhcp/coredhcp/plugins"
|
||||
"github.com/insomniacslk/dhcp/dhcpv4"
|
||||
)
|
||||
|
||||
var log = logger.GetLogger("plugins/staticroute")
|
||||
|
||||
// Plugin wraps the information necessary to register a plugin.
|
||||
var Plugin = plugins.Plugin{
|
||||
Name: "staticroute",
|
||||
Setup4: setup4,
|
||||
}
|
||||
|
||||
var routes dhcpv4.Routes
|
||||
|
||||
func setup4(args ...string) (handler.Handler4, error) {
|
||||
log.Printf("loaded plugin for DHCPv4.")
|
||||
routes = make(dhcpv4.Routes, 0)
|
||||
|
||||
if len(args) < 1 {
|
||||
return nil, errors.New("need at least one static route")
|
||||
}
|
||||
|
||||
var err error
|
||||
for _, arg := range args {
|
||||
fields := strings.Split(arg, ",")
|
||||
if len(fields) != 2 {
|
||||
return Handler4, errors.New("expected a destination/gateway pair, got: " + arg)
|
||||
}
|
||||
|
||||
route := &dhcpv4.Route{}
|
||||
_, route.Dest, err = net.ParseCIDR(fields[0])
|
||||
if err != nil {
|
||||
return Handler4, errors.New("expected a destination subnet, got: " + fields[0])
|
||||
}
|
||||
|
||||
route.Router = net.ParseIP(fields[1])
|
||||
if route.Router == nil {
|
||||
return Handler4, errors.New("expected a gateway address, got: " + fields[1])
|
||||
}
|
||||
|
||||
routes = append(routes, route)
|
||||
log.Debugf("adding static route %s", route)
|
||||
}
|
||||
|
||||
log.Printf("loaded %d static routes.", len(routes))
|
||||
|
||||
return Handler4, nil
|
||||
}
|
||||
|
||||
// Handler4 handles DHCPv4 packets for the static routes plugin
|
||||
func Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) {
|
||||
if len(routes) > 0 {
|
||||
resp.Options.Update(dhcpv4.Option{
|
||||
Code: dhcpv4.OptionCode(dhcpv4.OptionClasslessStaticRoute),
|
||||
Value: routes,
|
||||
})
|
||||
}
|
||||
|
||||
return resp, false
|
||||
}
|
||||
60
plugins/staticroute/plugin_test.go
Normal file
60
plugins/staticroute/plugin_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 staticroute
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSetup4(t *testing.T) {
|
||||
assert.Empty(t, routes)
|
||||
|
||||
var err error
|
||||
// no args
|
||||
_, err = setup4()
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, "need at least one static route", err.Error())
|
||||
}
|
||||
|
||||
// invalid arg
|
||||
_, err = setup4("foo")
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, "expected a destination/gateway pair, got: foo", err.Error())
|
||||
}
|
||||
|
||||
// invalid destination
|
||||
_, err = setup4("foo,")
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, "expected a destination subnet, got: foo", err.Error())
|
||||
}
|
||||
|
||||
// invalid gateway
|
||||
_, err = setup4("10.0.0.0/8,foo")
|
||||
if assert.Error(t, err) {
|
||||
assert.Equal(t, "expected a gateway address, got: foo", err.Error())
|
||||
}
|
||||
|
||||
// valid route
|
||||
_, err = setup4("10.0.0.0/8,192.168.1.1")
|
||||
if assert.NoError(t, err) {
|
||||
if assert.Equal(t, 1, len(routes)) {
|
||||
assert.Equal(t, "10.0.0.0/8", routes[0].Dest.String())
|
||||
assert.Equal(t, "192.168.1.1", routes[0].Router.String())
|
||||
}
|
||||
}
|
||||
|
||||
// multiple valid routes
|
||||
_, err = setup4("10.0.0.0/8,192.168.1.1", "192.168.2.0/24,192.168.1.100")
|
||||
if assert.NoError(t, err) {
|
||||
if assert.Equal(t, 2, len(routes)) {
|
||||
assert.Equal(t, "10.0.0.0/8", routes[0].Dest.String())
|
||||
assert.Equal(t, "192.168.1.1", routes[0].Router.String())
|
||||
assert.Equal(t, "192.168.2.0/24", routes[1].Dest.String())
|
||||
assert.Equal(t, "192.168.1.100", routes[1].Router.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user