Files
coredhcp/plugins/prefix/plugin.go
2025-06-30 09:23:28 +08:00

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
}