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:
1 parent
29fc991ce1
commit
8ee430276c
29 files changed
+653
-79
No files matched your search
@@ -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))
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package model
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParsePoolOrder(t *testing.T) {
|
||||
for in, want := range map[string]string{
|
||||
"10.0.0.1-10.0.0.50": "10.0.0.1-10.0.0.50",
|
||||
"10.0.0.1 - 10.0.0.50:Random": "10.0.0.1-10.0.0.50:random",
|
||||
"10.0.0.0/26:reverse": "10.0.0.0-10.0.0.63:reverse",
|
||||
"10.0.0.1-10.0.0.50:default": "10.0.0.1-10.0.0.50",
|
||||
} {
|
||||
p, err := ParsePool(in)
|
||||
if err != nil || p.String() != want {
|
||||
t.Errorf("%q: %v %v", in, p, err)
|
||||
}
|
||||
}
|
||||
for _, in := range []string{"10.0.0.1-10.0.0.50:", "10.0.0.1-10.0.0.50:lowest"} {
|
||||
if _, err := ParsePool(in); err == nil {
|
||||
t.Errorf("%q accepted", in)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user