Files
headscale/hscontrol/db/ip_random_exhaustion_test.go
T
Kristoffer Dalby f61753e737 db: bound IP allocation scan so exhausted prefixes error out
Random strategy previously spun forever under the allocator mutex.
2026-06-08 10:04:49 +02:00

53 lines
1.5 KiB
Go

package db
import (
"errors"
"net/netip"
"testing"
"time"
"github.com/juanfont/headscale/hscontrol/types"
)
// TestIPAllocatorRandomExhaustionReturnsError ensures the random allocation
// strategy terminates when its prefix is exhausted. randomNext only ever
// returns in-prefix addresses, so the allocation loop's exhaustion exit must
// not depend on producing an out-of-prefix candidate; otherwise Next() spins
// forever under the allocator mutex, wedging all registration and IP release.
//
// 100.64.0.0/30 has four addresses: .0 (network) and .3 (broadcast) are
// reserved, leaving .1 and .2. After two allocations the pool is exhausted and
// the third Next() must return ErrCouldNotAllocateIP promptly.
func TestIPAllocatorRandomExhaustionReturnsError(t *testing.T) {
prefix4 := netip.MustParsePrefix("100.64.0.0/30")
alloc, err := NewIPAllocator(nil, &prefix4, nil, types.IPAllocationStrategyRandom)
if err != nil {
t.Fatalf("NewIPAllocator: %v", err)
}
for i := range 2 {
_, _, err := alloc.Next()
if err != nil {
t.Fatalf("Next() #%d unexpectedly failed: %v", i+1, err)
}
}
done := make(chan error, 1)
go func() {
_, _, err := alloc.Next()
done <- err
}()
select {
case err := <-done:
if !errors.Is(err, ErrCouldNotAllocateIP) {
t.Fatalf("expected ErrCouldNotAllocateIP on exhausted prefix, got: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("Next() on an exhausted random-strategy prefix did not return " +
"within 2s; it is spinning forever under the allocator mutex")
}
}