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

+75 -2
View File
@@ -48,6 +48,45 @@ type Settings struct {
// addresses that don't belong on the interface, for networks where
// another DHCP server shares the segment. Off by default.
NotAuthoritative bool `json:"not_authoritative"`
// AddressOrder is which never-used pool address a new client gets:
// sequential (the lowest, the default), reverse (the highest) or
// random. Subnets and pools can set their own.
AddressOrder string `json:"address_order,omitempty"`
}
// Address orders: which never-used pool address a new client gets. When
// every free address has been used before, the one free the longest is
// given out, whatever the order.
const (
OrderSequential = "sequential"
OrderReverse = "reverse"
OrderRandom = "random"
)
// Pool orders: which of a subnet's pools a new address comes from.
const (
// PoolsListed tries the pools in the order they are listed.
PoolsListed = "listed"
// PoolsRandom picks a pool at random, weighted by its never-used
// addresses, so addresses spread over the whole subnet.
PoolsRandom = "random"
)
// AddressOrders are the address orders, for messages and help.
var AddressOrders = []string{OrderSequential, OrderReverse, OrderRandom}
// ValidAddressOrder reports whether s is an address order ("" too: the
// default or inherited one).
func ValidAddressOrder(s string) bool {
return s == "" || s == OrderSequential || s == OrderReverse || s == OrderRandom
}
// EffectiveAddressOrder is the settings' address order with its default.
func (s Settings) EffectiveAddressOrder() string {
if s.AddressOrder == "" {
return OrderSequential
}
return s.AddressOrder
}
// DefaultSettings are the settings of a new database.
@@ -101,6 +140,12 @@ type Subnet struct {
Interface string `json:"interface"`
Prefix netip.Prefix `json:"prefix"`
Pools []Pool `json:"pools"`
// PoolOrder is which pool a new address comes from: listed (the
// default, "") or random.
PoolOrder string `json:"pool_order,omitempty"`
// AddressOrder overrides the settings' address order inside the
// subnet's pools ("": inherit); a pool can override it in turn.
AddressOrder string `json:"address_order,omitempty"`
// LeaseTime overrides the settings' lease time (0: inherit).
LeaseTime int `json:"lease_time"`
Options []OptionValue `json:"options"`
@@ -122,9 +167,25 @@ func (s *Subnet) Label() string {
type Pool struct {
Start netip.Addr `json:"start"`
End netip.Addr `json:"end"`
// Order overrides the subnet's address order in this pool ("":
// inherit).
Order string `json:"order,omitempty"`
}
func (p Pool) String() string { return p.Start.String() + "-" + p.End.String() }
// String is the pool as ParsePool reads it: FIRST-LAST, with :ORDER when
// it has its own.
func (p Pool) String() string {
if p.Order != "" {
return p.Range() + ":" + p.Order
}
return p.Range()
}
// Range is FIRST-LAST.
func (p Pool) Range() string { return p.Start.String() + "-" + p.End.String() }
// SameRange reports whether two pools cover the same addresses.
func (p Pool) SameRange(o Pool) bool { return p.Start == o.Start && p.End == o.End }
// Contains reports whether a is in the pool.
func (p Pool) Contains(a netip.Addr) bool {
@@ -140,9 +201,21 @@ func (p Pool) Size() int {
}
// ParsePool reads "10.0.10.100-10.0.10.199" (spaces around the dash are
// fine) or a prefix, "10.0.10.128/25".
// fine) or a prefix, "10.0.10.128/25", optionally followed by the pool's
// own address order: "10.0.10.100-10.0.10.199:random".
func ParsePool(s string) (Pool, error) {
s = strings.TrimSpace(s)
if r, order, ok := strings.Cut(s, ":"); ok {
order = strings.ToLower(strings.TrimSpace(order))
if order == "default" {
order = "" // the subnet's
} else if order == "" || !ValidAddressOrder(order) {
return Pool{}, fmt.Errorf("pool %q: the order after the colon is %s or default (the subnet's)", s, strings.Join(AddressOrders, ", "))
}
p, err := ParsePool(r)
p.Order = order
return p, err
}
if a, b, ok := strings.Cut(s, "-"); ok {
start, err1 := netip.ParseAddr(strings.TrimSpace(a))
end, err2 := netip.ParseAddr(strings.TrimSpace(b))