/opt/splicevpn/coordinator/ipalloc.go
package main
import (
"net"
"sync"
)
// ipAllocator hands out sequential /32 addresses from the overlay range and
// recycles released ones. MVP-simple: linear scan, fine for thousands of nodes.
type ipAllocator struct {
mu sync.Mutex
base net.IP
mask net.IPMask
cursor uint32
used map[uint32]bool
}
func newIPAllocator(cidr *net.IPNet) *ipAllocator {
return &ipAllocator{
base: cidr.IP.Mask(cidr.Mask).To4(),
mask: cidr.Mask,
used: make(map[uint32]bool),
}
}
// next returns the next free address, or nil if the range is exhausted.
func (a *ipAllocator) next() net.IP {
a.mu.Lock()
defer a.mu.Unlock()
ones, bits := a.mask.Size()
host := uint32(1) << (bits - ones)
for off := uint32(1); off < host-1; off++ { // skip network + broadcast
if a.used[off] {
continue
}
a.used[off] = true
return offsetIP(a.base, off)
}
return nil
}
func (a *ipAllocator) reserve(ip net.IP) {
if ip == nil {
return
}
a.mu.Lock()
a.used[ipOffset(a.base, ip)] = true
a.mu.Unlock()
}
func (a *ipAllocator) release(ip net.IP) {
a.mu.Lock()
delete(a.used, ipOffset(a.base, ip))
a.mu.Unlock()
}
func offsetIP(base net.IP, off uint32) net.IP {
b := make(net.IP, 4)
v := ipToU32(base) + off
b[0] = byte(v >> 24)
b[1] = byte(v >> 16)
b[2] = byte(v >> 8)
b[3] = byte(v)
return b
}
func ipOffset(base, ip net.IP) uint32 { return ipToU32(ip.To4()) - ipToU32(base) }
func ipToU32(ip net.IP) uint32 {
ip = ip.To4()
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
}