~bigbes/shroud

ref: f351e0e2219a80b39e0a2f018ccdeb4c059ad133 shroud/internal/awgserver/ipalloc.go -rw-r--r-- 1.5 KiB
f351e0e2 — Eugene Blikh refactor: rename project to shroud 2 months ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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())
}