whereNotNull('wg_ip')->pluck('wg_ip')->all(), // withTrashed: a revoked peer keeps its address until the hub drops it. VpnPeer::withTrashed()->pluck('allowed_ip')->all(), )); $hubIp = (string) config('provisioning.wireguard.hub_ip'); [$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; } } throw new RuntimeException('WireGuard management subnet exhausted.'); } public function addPeer(string $publicKey, string $ip): void { Process::run(['wg', 'set', 'wg0', 'peer', $publicKey, 'allowed-ips', $ip.'/32'])->throw(); Process::run('wg-quick save wg0')->throw(); // persist, else the peer is lost on restart } public function removePeer(string $publicKey): void { Process::run(['wg', 'set', 'wg0', 'peer', $publicKey, 'remove'])->throw(); Process::run('wg-quick save wg0')->throw(); // persist, else the peer is lost on restart } public function peers(): array { $result = Process::run(['wg', 'show', 'wg0', 'dump']); if (! $result->successful()) { throw new RuntimeException('wg show failed: '.trim($result->errorOutput())); } $peers = []; // The first line describes the interface itself, not a peer. foreach (array_slice(preg_split('/\R/', trim($result->output())) ?: [], 1) as $line) { if (trim($line) === '') { continue; } $snapshot = PeerSnapshot::fromDumpLine(explode("\t", $line)); if ($snapshot !== null) { $peers[$snapshot->publicKey] = $snapshot; } } return $peers; } public function endpoint(): string { return ProvisioningSettings::wgEndpoint(); } public function publicKey(): string { return ProvisioningSettings::wgHubPublicKey(); } }