add all files from Hong

This commit is contained in:
zhangsz
2025-06-30 09:23:28 +08:00
parent ceb1fe2640
commit 9b7d32fbd9
69 changed files with 7280 additions and 0 deletions

View 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")

View 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
}

View 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
}

View 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)
}
}

View 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()
}
}
})
}

View 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
}

View 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,
)
}
}

View 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
}

View 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
View 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
View 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
View 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
View 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
View 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))
})
}

View 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
}

View 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")
}
}

View 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
View 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
}

View 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
View 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
View 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
}

View 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
View 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
View 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
}

View 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
View 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
View 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
}

View 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
View 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
}

View 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
View 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
}

View 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
}

View 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
View 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
}

View 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
View 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
}
}

View 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
}

View 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())
}
}
}