From 8a392bfd066aceb91f1e0ecf3940e2c0545141e6 Mon Sep 17 00:00:00 2001 From: nexxo Date: Sat, 25 Jul 2026 10:48:16 +0200 Subject: [PATCH] fix(engine): parse WireGuard CIDR for address allocation (any prefix length) Co-Authored-By: Claude Opus 4.8 --- app/Services/Wireguard/LocalWireguardHub.php | 18 ++++++++++++------ tests/Feature/Provisioning/ServicesTest.php | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/app/Services/Wireguard/LocalWireguardHub.php b/app/Services/Wireguard/LocalWireguardHub.php index 7433a7b..0376674 100644 --- a/app/Services/Wireguard/LocalWireguardHub.php +++ b/app/Services/Wireguard/LocalWireguardHub.php @@ -4,7 +4,6 @@ namespace App\Services\Wireguard; use App\Models\Host; use Illuminate\Support\Facades\Process; -use Illuminate\Support\Str; use RuntimeException; /** @@ -17,12 +16,19 @@ class LocalWireguardHub implements WireguardHub { public function allocateIp(): string { - $used = Host::query()->whereNotNull('wg_ip')->pluck('wg_ip')->all(); - $prefix = Str::beforeLast(config('provisioning.wireguard.subnet', '10.66.0.0/24'), '.'); + $used = array_flip(Host::query()->whereNotNull('wg_ip')->pluck('wg_ip')->all()); + $hubIp = (string) config('provisioning.wireguard.hub_ip'); - for ($octet = 2; $octet <= 254; $octet++) { - $ip = "{$prefix}.{$octet}"; - if (! in_array($ip, $used, true)) { + [$network, $bits] = explode('/', config('provisioning.wireguard.subnet', '10.66.0.0/24')); + $bits = (int) $bits; + $hostBits = 32 - $bits; + $size = 2 ** $hostBits; + $base = ip2long($network) & (0xFFFFFFFF << $hostBits) & 0xFFFFFFFF; + + // Skip the network address (offset 0) and broadcast (offset size-1). + for ($offset = 1; $offset < $size - 1; $offset++) { + $ip = long2ip($base + $offset); + if ($ip !== $hubIp && ! isset($used[$ip])) { return $ip; } } diff --git a/tests/Feature/Provisioning/ServicesTest.php b/tests/Feature/Provisioning/ServicesTest.php index 904e3d6..60e12f7 100644 --- a/tests/Feature/Provisioning/ServicesTest.php +++ b/tests/Feature/Provisioning/ServicesTest.php @@ -5,6 +5,7 @@ use App\Services\Proxmox\FakeProxmoxClient; use App\Services\Ssh\CommandResult; use App\Services\Ssh\FakeRemoteShell; use App\Services\Wireguard\FakeWireguardHub; +use App\Services\Wireguard\LocalWireguardHub; it('scripts remote command output and records calls (FakeRemoteShell)', function () { $shell = new FakeRemoteShell; @@ -34,6 +35,23 @@ it('allocates ips and tracks peers (FakeWireguardHub)', function () { expect($hub->peers())->toBe([]); }); +it('allocates addresses within a /24 subnet, skipping hub and used (LocalWireguardHub)', function () { + config()->set('provisioning.wireguard.subnet', '10.66.0.0/24'); + config()->set('provisioning.wireguard.hub_ip', '10.66.0.1'); + Host::factory()->create(['wg_ip' => '10.66.0.2']); + + // .0 network, .1 hub, .2 used → first free is .3 + expect((new LocalWireguardHub)->allocateIp())->toBe('10.66.0.3'); +}); + +it('respects a non-/24 subnet (LocalWireguardHub)', function () { + config()->set('provisioning.wireguard.subnet', '10.66.5.128/25'); + config()->set('provisioning.wireguard.hub_ip', '10.66.5.129'); + + // network .128, hub .129 → first free is .130 + expect((new LocalWireguardHub)->allocateIp())->toBe('10.66.5.130'); +}); + it('reports node capacity for a host (FakeProxmoxClient)', function () { $host = Host::factory()->create(['wg_ip' => '10.66.0.5']); $client = (new FakeProxmoxClient)->forHost($host);