package awgserver import ( "fmt" "net/netip" ) // IPAllocator assigns IPs from a CIDR subnet for WireGuard peers. type IPAllocator struct { prefix netip.Prefix } // NewIPAllocator creates an allocator for the given CIDR (e.g., "10.14.0.0/24"). func NewIPAllocator(cidr string) (*IPAllocator, error) { prefix, err := netip.ParsePrefix(cidr) if err != nil { return nil, fmt.Errorf("parse CIDR %q: %w", cidr, err) } prefix = prefix.Masked() return &IPAllocator{prefix: prefix}, nil } // ServerIP returns the first usable IP in the subnet (network + 1), used as the server address. func (a *IPAllocator) ServerIP() netip.Addr { return a.prefix.Addr().Next() } // Allocate finds the next free IP in the subnet, skipping the network address, // server address (.1), broadcast, and any IPs in the used set. func (a *IPAllocator) Allocate(used []string) (string, error) { usedSet := make(map[netip.Addr]struct{}, len(used)) for _, u := range used { p, err := netip.ParsePrefix(u) if err != nil { continue } usedSet[p.Addr()] = struct{}{} } // Start from network + 2 (skip network addr and server addr). addr := a.prefix.Addr().Next().Next() for a.prefix.Contains(addr) { next := addr.Next() // Skip if next would be outside prefix (broadcast equivalent). if !a.prefix.Contains(next) { break } if _, taken := usedSet[addr]; !taken { return fmt.Sprintf("%s/32", addr.String()), nil } addr = next } return "", fmt.Errorf("no free IPs in %s", a.prefix.String()) }