1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 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
}
|