-
Notifications
You must be signed in to change notification settings - Fork 108
/
plugin.go
271 lines (232 loc) · 8.4 KB
/
plugin.go
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
}