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
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
package subnetallocator
import (
"encoding/json"
"errors"
"fmt"
"net/netip"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/johnandersen777/socialweb-computer-kcp/internal/hostos"
)
const (
PoolBase = "10.100.0.0/16"
LeaseBits = 24
leaseFileVersion = 1
leaseDirMode = 0o700
leaseFileMode = 0o600
)
func DefaultPool() Pool {
return Pool{Base: netip.MustParsePrefix(PoolBase), Bits: LeaseBits}
}
type Pool struct {
Base netip.Prefix
Bits int
}
func (p Pool) Validate() error {
if !p.Base.IsValid() {
return fmt.Errorf("subnetallocator: pool %v is not a CIDR block, so there is no space to walk and "+
"no way to compare two ranges for overlap; a pool that cannot be parsed is a pool that "+
"silently contains everything", p.Base)
}
if !p.Base.Addr().Is4() {
return fmt.Errorf("subnetallocator: pool %s is not IPv4. Every range this package leases "+
"addresses a tap behind pasta: the guest's address, its netmask and the gateway it dials "+
"this host through are the three values pasta's -a, -n and -g carry, and those are IPv4 "+
"values in a firecracker guest whose kernel is booted with no IPv6 configuration of ours. "+
"A pool of the wrong family leases ranges nothing can be addressed in", p.Base)
}
if p.Bits < p.Base.Bits() {
return fmt.Errorf("subnetallocator: pool %s cannot be cut into /%d ranges, because a range "+
"wider than the pool it comes from is not inside the pool. Every range must sit wholly "+
"inside the pool, since the disjointness of two ranges is proved by the disjointness of "+
"their blocks and that proof needs both of them enumerated over the same space",
p.Base, p.Bits)
}
if p.Bits > 30 {
return fmt.Errorf("subnetallocator: pool %s cannot be cut into /%d ranges: a /%d holds %d "+
"addresses and a range needs one for the gateway pasta hands the guest as its default "+
"route and at least one for a node, after the network and broadcast addresses are taken "+
"out. The widest range that leaves a node an address is a /30",
p.Base, p.Bits, p.Bits, 1<<uint(32-p.Bits))
}
return nil
}
func (p Pool) Masked() netip.Prefix { return p.Base.Masked() }
func (p Pool) BlockSize() uint64 {
if err := p.Validate(); err != nil {
return 0
}
return uint64(1) << uint(32-p.Bits)
}
func (p Pool) Blocks() uint64 {
if err := p.Validate(); err != nil {
return 0
}
return uint64(1) << uint(p.Bits-p.Base.Bits())
}
func (p Pool) BlockAt(i uint64) netip.Prefix {
return netip.PrefixFrom(addOffset(p.Masked().Addr(), uint32(i*p.BlockSize())), p.Bits)
}
func (p Pool) Contains(block netip.Prefix) bool {
if err := p.Validate(); err != nil {
return false
}
if !block.IsValid() || !block.Addr().Is4() {
return false
}
if block.Bits() < p.Base.Bits() {
return false
}
return p.Masked().Contains(block.Masked().Addr())
}
func (p Pool) String() string {
blocks := p.Blocks()
if blocks == 1 {
return fmt.Sprintf("%s, a single range of /%d", p.Masked(), p.Bits)
}
return fmt.Sprintf("%s, %d ranges of /%d", p.Masked(), blocks, p.Bits)
}
func addOffset(addr netip.Addr, off uint32) netip.Addr {
v := addr.As4()
x := uint32(v[0])<<24 | uint32(v[1])<<16 | uint32(v[2])<<8 | uint32(v[3])
x += off
return netip.AddrFrom4([4]byte{byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x)})
}
type Lease struct {
Cluster string
AppInstance string
Block netip.Prefix
AllocatedAt time.Time
}
func (l Lease) Gateway() netip.Addr {
return addOffset(l.Block.Masked().Addr(), 1)
}
func (l Lease) PrefixBits() int { return l.Block.Bits() }
func (l Lease) Netmask() netip.Addr {
ones := l.Block.Bits()
var v uint32
if ones > 0 && ones < 32 {
v = ^uint32(0) << uint(32-ones)
}
return netip.AddrFrom4([4]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}
func (l Lease) HostCount() int {
if !l.Block.IsValid() || !l.Block.Addr().Is4() {
return 0
}
return (1 << uint(32-l.Block.Bits())) - 2
}
func (l Lease) Host(n int) (netip.Addr, error) {
if !l.Block.IsValid() || !l.Block.Addr().Is4() {
return netip.Addr{}, fmt.Errorf("subnetallocator: cluster %s holds %v, which is not an IPv4 "+
"range, so no guest address can be taken from it", l.Cluster, l.Block)
}
count := l.HostCount()
if n < 1 || n > count {
return netip.Addr{}, fmt.Errorf("subnetallocator: cluster %s's range %s holds %d usable "+
"addresses numbered 1 to %d, where 1 is the gateway pasta hands the guest as its default "+
"route and the rest are the nodes; %d is outside that range. The nodes of one cluster are "+
"the only addresses that range will ever hold, so an address taken from outside it is a "+
"guest sharing an address with a node of another cluster or with this host",
l.Cluster, l.Block.Masked(), count, count, n)
}
return addOffset(l.Block.Masked().Addr(), uint32(n)), nil
}
func (l Lease) Contains(a netip.Addr) bool { return l.Block.Masked().Contains(a) }
func (l Lease) String() string {
return fmt.Sprintf("%s held by cluster %s", l.Block.Masked(), l.Cluster)
}
type Request struct {
Cluster string
}
type Config struct {
LeasePath string
AppInstance string
HostNetworks func() ([]HostNetwork, error)
Now func() time.Time
}
type leaseKey struct {
app string
cluster string
}
type Allocator struct {
app string
path string
pool Pool
hosts func() ([]HostNetwork, error)
now func() time.Time
mu sync.Mutex
leases map[leaseKey]Lease
}
type leaseRecord struct {
Cluster string `json:"cluster"`
AppInstance string `json:"appInstance,omitempty"`
Block string `json:"block"`
AllocatedAt time.Time `json:"allocatedAt"`
}
type leaseFile struct {
Version int `json:"version"`
Pool string `json:"pool"`
Leases []leaseRecord `json:"leases"`
}
func Open(cfg Config) (*Allocator, error) {
return openWithPool(cfg, DefaultPool())
}
func openWithPool(cfg Config, pool Pool) (*Allocator, error) {
if cfg.LeasePath == "" {
return nil, errors.New("subnetallocator: LeasePath is required. The host-side lease file is " +
"the only record of which cluster holds which range, so a process that names none leases " +
"from a pool with no memory: it hands the same range out twice, and the second cluster's " +
"nodes are addressed exactly as the first cluster's nodes with nothing in either cluster " +
"reporting it")
}
if err := pool.Validate(); err != nil {
return nil, err
}
a := &Allocator{
app: cfg.AppInstance,
path: cfg.LeasePath,
pool: pool,
hosts: cfg.HostNetworks,
now: cfg.Now,
leases: map[leaseKey]Lease{},
}
if a.hosts == nil {
a.hosts = HostNetworks
}
if a.now == nil {
a.now = time.Now
}
if _, err := a.hosts(); err != nil {
return nil, fmt.Errorf("subnetallocator: enumerating the networks this host carries, before "+
"this gateway leases anything: %w. Every range this package hands out is non-conflicting "+
"only because it was compared against the host's own networks, so a host whose networks "+
"cannot be read is a host against which no range can be checked. Refusing here rather "+
"than at the first allocation, because a gateway that starts is a gateway an operator "+
"believes, and this one would start on the strength of an enumeration that failed", err)
}
if err := a.load(); err != nil {
return nil, err
}
return a, nil
}
func (a *Allocator) Pool() Pool { return a.pool }
func (a *Allocator) lockPath() string { return a.path + ".lock" }
func (a *Allocator) withLock(how int, fn func() error) error {
if err := os.MkdirAll(filepath.Dir(a.path), leaseDirMode); err != nil {
return fmt.Errorf("subnetallocator: lease dir: %w", err)
}
f, err := os.OpenFile(a.lockPath(), os.O_RDWR|os.O_CREATE, leaseFileMode)
if err != nil {
return fmt.Errorf("subnetallocator: lease lock %s: %w", a.lockPath(), err)
}
defer f.Close()
if err := hostos.Lock(f, how); err != nil {
return fmt.Errorf("subnetallocator: locking %s: %w", a.lockPath(), err)
}
defer func() { _ = hostos.Unlock(f) }()
return fn()
}
func (a *Allocator) load() error {
leases, err := a.readFile()
if err != nil {
return err
}
a.leases = leases
return nil
}
func (a *Allocator) readFile() (map[leaseKey]Lease, error) {
data, err := os.ReadFile(a.path)
if errors.Is(err, os.ErrNotExist) {
return map[leaseKey]Lease{}, nil
}
if err != nil {
return nil, fmt.Errorf("subnetallocator: reading the lease file %s: %w", a.path, err)
}
var f leaseFile
if err := json.Unmarshal(data, &f); err != nil {
return nil, fmt.Errorf("subnetallocator: the lease file %s is unreadable, refusing to start "+
"with an empty store over it: %w. Starting empty does not fail -- it quietly leases a "+
"SECOND range to every cluster this file names, and the range each of those clusters is "+
"already addressed in is then held by nobody, so the next cluster that walks the pool is "+
"handed a range live nodes are still in", a.path, err)
}
if f.Version != leaseFileVersion {
return nil, fmt.Errorf("subnetallocator: the lease file %s is version %d and this build "+
"writes %d; refusing to read it, because the fields this build does not know about are "+
"the ranges it would fail to see and hand out a second time",
a.path, f.Version, leaseFileVersion)
}
if f.Pool != a.pool.Masked().String() {
return nil, fmt.Errorf("subnetallocator: the lease file %s records ranges leased from pool "+
"%s, and this build leases from pool %s. Refusing to start, because the two are different "+
"spaces: continuing would walk the new pool while the record still holds blocks of the old "+
"one, so a range already leased would not be seen and would be handed to a second cluster. "+
"The pool is a constant in this package's code, so a change here is a migration: move or "+
"release the leases in that file by hand, with the clusters they belong to named in it",
a.path, f.Pool, a.pool.Masked())
}
leases := make(map[leaseKey]Lease, len(f.Leases))
for _, r := range f.Leases {
if r.Cluster == "" {
return nil, fmt.Errorf("subnetallocator: the lease file %s records a range %q held by no "+
"cluster. A range is held by a cluster or it is free; one held by nothing can neither "+
"be leased again safely nor released, and it addresses nodes that are still running "+
"in it, so this refuses to start rather than walk the pool over it",
a.path, r.Block)
}
block, err := netip.ParsePrefix(r.Block)
if err != nil {
return nil, fmt.Errorf("subnetallocator: the lease file %s records %q for cluster %s, "+
"which is not a CIDR block: %w. Refusing to start over a store whose addresses cannot "+
"be read, because the alternative is leasing a SECOND range to a cluster that already "+
"holds one and moving its nodes' addresses out from under them",
a.path, r.Block, r.Cluster, err)
}
if !block.Addr().Is4() || block.Bits() != a.pool.Bits {
return nil, fmt.Errorf("subnetallocator: the lease file %s records %s for cluster %s, "+
"which is not a /%d IPv4 block. A lease is a range of the width this pool is cut "+
"into, so a range of another width is one that was never disjoint from a range of "+
"this one: it may overlap a block another cluster holds without this package being "+
"able to see it", a.path, block, r.Cluster, a.pool.Bits)
}
block = block.Masked()
if !a.pool.Contains(block) {
return nil, fmt.Errorf("subnetallocator: the lease file %s records %s for cluster %s, "+
"which is outside pool %s. A range outside the pool is a range the walk will never "+
"look at and never reconcile, so the block it occupies inside the pool if it "+
"overlaps one would be leased again to a second cluster and both would address "+
"their nodes alike. Refusing to start rather than leasing around a record that "+
"covers ground this build cannot see", a.path, block, r.Cluster, a.pool.Masked())
}
if prev, ok := leases[leaseKey{r.AppInstance, r.Cluster}]; ok {
return nil, fmt.Errorf("subnetallocator: the lease file %s names cluster %s twice, at %s "+
"and at %s. One of those two ranges is held by nobody and the pool walk will hand it "+
"to the next cluster, whose nodes are then addressed inside a range this cluster's "+
"nodes may still be in. Refusing to start rather than choosing one of the two, "+
"because the file is the only evidence of which range the cluster's nodes are "+
"actually addressed in and a guess here renumbers a running cluster",
a.path, r.Cluster, prev.Block, block)
}
leases[leaseKey{r.AppInstance, r.Cluster}] = Lease{Cluster: r.Cluster, AppInstance: r.AppInstance, Block: block, AllocatedAt: r.AllocatedAt}
}
return leases, nil
}
func (a *Allocator) save() error {
if err := os.MkdirAll(filepath.Dir(a.path), leaseDirMode); err != nil {
return fmt.Errorf("subnetallocator: lease dir: %w", err)
}
records := make([]leaseRecord, 0, len(a.leases))
for _, l := range a.snapshot() {
records = append(records, leaseRecord{
Cluster: l.Cluster,
AppInstance: l.AppInstance,
Block: l.Block.Masked().String(),
AllocatedAt: l.AllocatedAt,
})
}
data, err := json.MarshalIndent(leaseFile{
Version: leaseFileVersion,
Pool: a.pool.Masked().String(),
Leases: records,
}, "", " ")
if err != nil {
return err
}
tmp := a.path + ".tmp"
f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, leaseFileMode)
if err != nil {
return fmt.Errorf("subnetallocator: lease file: %w", err)
}
if _, err := f.Write(append(data, '\n')); err != nil {
f.Close()
return fmt.Errorf("subnetallocator: lease file: %w", err)
}
if err := f.Sync(); err != nil {
f.Close()
return fmt.Errorf("subnetallocator: lease file: %w", err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("subnetallocator: lease file: %w", err)
}
return os.Rename(tmp, a.path)
}
func (a *Allocator) Rekey(legacy, key string) (int, error) {
if legacy == "" || key == "" || legacy == key {
return 0, nil
}
a.mu.Lock()
defer a.mu.Unlock()
moved := 0
err := a.withLock(hostos.LockExclusive, func() error {
if err := a.load(); err != nil {
return err
}
held, ok := a.leases[leaseKey{a.app, legacy}]
if !ok {
return nil
}
if _, taken := a.leases[leaseKey{a.app, key}]; taken {
return nil
}
delete(a.leases, leaseKey{a.app, legacy})
held.Cluster = key
a.leases[leaseKey{a.app, key}] = held
moved = 1
return a.save()
})
if err != nil {
return 0, err
}
return moved, nil
}
func (a *Allocator) Allocate(req Request) (Lease, error) {
if req.Cluster == "" {
return Lease{}, errors.New("subnetallocator: Cluster is required. A range is leased to a " +
"cluster and looked up by that name on every later start, so an unnamed lease can never " +
"be found again: the same cluster on its next start would be handed a second range and " +
"its nodes would move out of the first one while that range stayed leased to nobody")
}
a.mu.Lock()
defer a.mu.Unlock()
var granted Lease
err := a.withLock(hostos.LockExclusive, func() error {
if err := a.load(); err != nil {
return err
}
if found := FindDuplicateBlocks(a.snapshot()); len(found) > 0 {
return errors.New(strings.Join(found, "; "))
}
if held, ok := a.leases[leaseKey{a.app, req.Cluster}]; ok {
granted = held
return nil
}
hosts, err := a.hosts()
if err != nil {
return fmt.Errorf("subnetallocator: enumerating the networks this host carries, before "+
"leasing a range to cluster %s: %w. Every range handed out here is non-conflicting "+
"only because it was compared against the host's own networks, so leasing on an "+
"enumeration that failed is leasing a range nobody checked. Nothing is leased and "+
"nothing is written", req.Cluster, err)
}
blocks := a.pool.Blocks()
exhausted := &PoolExhaustedError{Pool: a.pool, Candidates: blocks}
for i := range blocks {
candidate := a.pool.BlockAt(i)
err := a.checkLocked(candidate, hosts)
var hostErr *HostCollisionError
var leaseErr *LeaseCollisionError
switch {
case err == nil:
lease := Lease{
Cluster: req.Cluster,
AppInstance: a.app,
Block: candidate,
AllocatedAt: a.now(),
}
a.leases[leaseKey{a.app, req.Cluster}] = lease
if saveErr := a.save(); saveErr != nil {
delete(a.leases, leaseKey{a.app, req.Cluster})
return saveErr
}
granted = lease
return nil
case errors.As(err, &hostErr):
exhausted.HostSkips++
if exhausted.HostFirst == nil {
exhausted.HostFirst = hostErr
}
case errors.As(err, &leaseErr):
exhausted.LeaseSkips++
if exhausted.LeaseFirst == nil {
exhausted.LeaseFirst = leaseErr
}
default:
return err
}
}
return exhausted
})
if err != nil {
return Lease{}, err
}
return granted, nil
}
func (a *Allocator) Check(block netip.Prefix) error {
hosts, err := a.hosts()
if err != nil {
return fmt.Errorf("subnetallocator: enumerating the networks this host carries, before "+
"checking %s: %w. A range is non-conflicting only against an enumeration that succeeded, "+
"so this reports the enumeration's failure rather than the check it could not do", block, err)
}
a.mu.Lock()
defer a.mu.Unlock()
a.refresh()
return a.checkLocked(block, hosts)
}
func (a *Allocator) Release(cluster string) error {
a.mu.Lock()
defer a.mu.Unlock()
return a.withLock(hostos.LockExclusive, func() error {
if err := a.load(); err != nil {
return err
}
if _, ok := a.leases[leaseKey{a.app, cluster}]; !ok {
return nil
}
delete(a.leases, leaseKey{a.app, cluster})
return a.save()
})
}
func (a *Allocator) AllLeases() []Lease {
a.mu.Lock()
defer a.mu.Unlock()
a.refresh()
return a.snapshot()
}
func (a *Allocator) Leases() []Lease {
a.mu.Lock()
defer a.mu.Unlock()
a.refresh()
var out []Lease
for _, l := range a.snapshot() {
if l.AppInstance == a.app {
out = append(out, l)
}
}
return out
}
func (a *Allocator) LeaseFor(cluster string) (Lease, bool) {
a.mu.Lock()
defer a.mu.Unlock()
a.refresh()
l, ok := a.leases[leaseKey{a.app, cluster}]
return l, ok
}
func (a *Allocator) refresh() {
_ = a.withLock(hostos.LockShared, a.load)
}
func (a *Allocator) snapshot() []Lease {
out := make([]Lease, 0, len(a.leases))
for _, l := range a.leases {
out = append(out, l)
}
sort.Slice(out, func(i, j int) bool {
if out[i].Block != out[j].Block {
return lessBlock(out[i].Block, out[j].Block)
}
return out[i].Cluster < out[j].Cluster
})
return out
}
func lessBlock(a, b netip.Prefix) bool {
x, y := a.Masked().Addr(), b.Masked().Addr()
if !x.Is4() || !y.Is4() {
return x.String() < y.String()
}
xa, ya := x.As4(), y.As4()
for i := range 4 {
if xa[i] != ya[i] {
return xa[i] < ya[i]
}
}
return false
}