- Which never-used pool address a new client gets: sequential (the lowest, as before), reverse or random, in the settings, a subnet or a single pool (FIRST-LAST:ORDER). When no never-used address is left, the one free the longest, whatever the order; returning clients keep theirs. - A subnet's pools are used in the order listed, or one at random weighted by its never-used addresses. Pools now keep the order they are given in (no more sorting by address). - CLI --address-order and --pool-order; --add-pool of an existing range changes its order in place. Web UI: per-pool order and a move-up button, pool and address order on the subnet and settings pages. - Kea's allocator: random is imported. Schema migration 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
118 lines
2.7 KiB
Go
118 lines
2.7 KiB
Go
package dhcpd
|
|
|
|
import (
|
|
"net/netip"
|
|
"time"
|
|
|
|
"steward/internal/model"
|
|
)
|
|
|
|
// neverUsed picks a pool address that was never handed out (or whose
|
|
// lease was deleted), in the subnet's order (s.mu held): the pools listed
|
|
// or at random, and inside a pool sequential, reverse or random
|
|
// (docs/subnets.md, "Address order").
|
|
func (s *Server) neverUsed(cfg *config, own map[netip.Addr]bool, sub *subnet, cl client, now time.Time) (netip.Addr, bool) {
|
|
ok := func(ip netip.Addr) bool { return s.leases[ip] == nil && s.usable(cfg, own, sub, ip, cl, now) }
|
|
order := func(p model.Pool) string {
|
|
switch {
|
|
case p.Order != "":
|
|
return p.Order
|
|
case sub.AddressOrder != "":
|
|
return sub.AddressOrder
|
|
}
|
|
return cfg.settings.EffectiveAddressOrder()
|
|
}
|
|
if sub.PoolOrder != model.PoolsRandom {
|
|
for _, p := range sub.Pools {
|
|
if ip, found := s.pickIn(p, order(p), ok); found {
|
|
return ip, true
|
|
}
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
// A pool at random, weighted by its never-used addresses.
|
|
counts := make([]int, len(sub.Pools))
|
|
total := 0
|
|
for i, p := range sub.Pools {
|
|
counts[i] = countIn(p, ok)
|
|
total += counts[i]
|
|
}
|
|
if total == 0 {
|
|
return netip.Addr{}, false
|
|
}
|
|
n := s.randN(total)
|
|
for i, p := range sub.Pools {
|
|
if n < counts[i] {
|
|
return s.pickIn(p, order(p), ok)
|
|
}
|
|
n -= counts[i]
|
|
}
|
|
return netip.Addr{}, false
|
|
}
|
|
|
|
// pickIn picks an address of the pool that ok accepts, in an order.
|
|
func (s *Server) pickIn(p model.Pool, order string, ok func(netip.Addr) bool) (netip.Addr, bool) {
|
|
var found netip.Addr
|
|
switch order {
|
|
case model.OrderRandom:
|
|
n := countIn(p, ok)
|
|
if n == 0 {
|
|
return netip.Addr{}, false
|
|
}
|
|
k := s.randN(n)
|
|
eachIn(p, false, func(ip netip.Addr) bool {
|
|
if !ok(ip) {
|
|
return false
|
|
}
|
|
if k == 0 {
|
|
found = ip
|
|
return true
|
|
}
|
|
k--
|
|
return false
|
|
})
|
|
default:
|
|
eachIn(p, order == model.OrderReverse, func(ip netip.Addr) bool {
|
|
if ok(ip) {
|
|
found = ip
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
}
|
|
return found, found.IsValid()
|
|
}
|
|
|
|
// countIn counts the pool's addresses that ok accepts.
|
|
func countIn(p model.Pool, ok func(netip.Addr) bool) int {
|
|
n := 0
|
|
eachIn(p, false, func(ip netip.Addr) bool {
|
|
if ok(ip) {
|
|
n++
|
|
}
|
|
return false
|
|
})
|
|
return n
|
|
}
|
|
|
|
// eachIn calls fn for the pool's addresses, lowest first (highest first
|
|
// when reverse), until it returns true.
|
|
func eachIn(p model.Pool, reverse bool, fn func(netip.Addr) bool) {
|
|
if !p.Start.Is4() || !p.End.Is4() || p.End.Less(p.Start) {
|
|
return
|
|
}
|
|
lo, hi := model.U32(p.Start), model.U32(p.End)
|
|
if reverse {
|
|
for v := hi; ; v-- {
|
|
if fn(model.FromU32(v)) || v == lo {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
for v := lo; ; v++ {
|
|
if fn(model.FromU32(v)) || v == hi {
|
|
return
|
|
}
|
|
}
|
|
}
|