272 lines
8.4 KiB
Go
272 lines
8.4 KiB
Go
// 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
|
|
}
|