Address order: sequential, reverse or random, per subnet and pool

- 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>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-18 18:13:25 +01:00
1 parent 29fc991ce1
commit 8ee430276c
29 files changed
+653 -79

No files matched your search

+18 -6
View File
@@ -84,6 +84,9 @@ func (c *checker) settings(s model.Settings) {
if s.DeclineHold != 0 && (s.DeclineHold < 60 || s.DeclineHold > 30*24*3600) {
c.add(Error, "decline_hold_range", "settings", "the decline hold is %d seconds; use 1 minute to 30 days", s.DeclineHold)
}
if !model.ValidAddressOrder(s.AddressOrder) {
c.add(Error, "address_order", "settings", "the address order %q is not one of %s", s.AddressOrder, strings.Join(model.AddressOrders, ", "))
}
c.options(s.Options, "settings")
}
@@ -173,22 +176,31 @@ func (c *checker) pools(s *model.Subnet, obj string) {
if len(s.Pools) == 0 && !s.Disabled {
c.add(Warning, "subnet_no_pools", obj, "the subnet has no pools: only clients with a reservation get an address")
}
if s.PoolOrder != "" && s.PoolOrder != model.PoolsListed && s.PoolOrder != model.PoolsRandom {
c.add(Error, "pool_order", obj, "the pool order %q is not listed or random", s.PoolOrder)
}
if !model.ValidAddressOrder(s.AddressOrder) {
c.add(Error, "address_order", obj, "the address order %q is not one of %s", s.AddressOrder, strings.Join(model.AddressOrders, ", "))
}
network, broadcast := s.Prefix.Addr(), model.Last(s.Prefix)
for i, p := range s.Pools {
if !model.ValidAddressOrder(p.Order) {
c.add(Error, "address_order", obj, "pool %s: the address order %q is not one of %s", p.Range(), p.Order, strings.Join(model.AddressOrders, ", "))
}
if !p.Start.Is4() || !p.End.Is4() || p.End.Less(p.Start) {
c.add(Error, "pool_invalid", obj, "pool %s is not a range of IPv4 addresses", p)
c.add(Error, "pool_invalid", obj, "pool %s is not a range of IPv4 addresses", p.Range())
continue
}
if !s.Prefix.Contains(p.Start) || !s.Prefix.Contains(p.End) {
c.add(Error, "pool_outside_subnet", obj, "pool %s is not inside %s", p, s.Prefix)
c.add(Error, "pool_outside_subnet", obj, "pool %s is not inside %s", p.Range(), s.Prefix)
continue
}
if p.Contains(network) || p.Contains(broadcast) {
c.add(Error, "pool_network_address", obj, "pool %s includes the network address %s or the broadcast address %s; start it at %s and end it at %s at most", p, network, broadcast, network.Next(), broadcast.Prev())
c.add(Error, "pool_network_address", obj, "pool %s includes the network address %s or the broadcast address %s; start it at %s and end it at %s at most", p.Range(), network, broadcast, network.Next(), broadcast.Prev())
}
for _, o := range s.Pools[:i] {
if o.Start.Is4() && o.End.Is4() && !(p.End.Less(o.Start) || o.End.Less(p.Start)) {
c.add(Error, "pool_overlap", obj, "pools %s and %s overlap", o, p)
c.add(Error, "pool_overlap", obj, "pools %s and %s overlap", o.Range(), p.Range())
}
}
}
@@ -221,7 +233,7 @@ func (c *checker) routers(s *model.Subnet, global []model.OptionValue, obj strin
}
for _, p := range s.Pools {
if p.Contains(a) {
c.add(Warning, "router_in_pool", obj, "the router %s is inside pool %s; it is never handed out while it answers pings, but take it out of the pool", a, p)
c.add(Warning, "router_in_pool", obj, "the router %s is inside pool %s; it is never handed out while it answers pings, but take it out of the pool", a, p.Range())
}
}
}
@@ -242,7 +254,7 @@ func (c *checker) hostInterface(s *model.Subnet, env *Env, obj string) {
if s.Prefix.Contains(a.Addr()) {
for _, p := range s.Pools {
if p.Contains(a.Addr()) {
c.add(Warning, "pool_includes_server", obj, "pool %s includes this server's own address %s; it is never handed out, but take it out of the pool", p, a.Addr())
c.add(Warning, "pool_includes_server", obj, "pool %s includes this server's own address %s; it is never handed out, but take it out of the pool", p.Range(), a.Addr())
}
}
return