Source — /opt/splicevpn/coordinator/ipalloc.go

/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])
}