90 lines
3.1 KiB
PHP
90 lines
3.1 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Wireguard;
|
|
|
|
use App\Models\Host;
|
|
use App\Models\VpnPeer;
|
|
use Illuminate\Support\Facades\Process;
|
|
use RuntimeException;
|
|
|
|
/**
|
|
* Real hub on the CluPilot VM. Allocates the next free address in the configured
|
|
* subnet and manages peers with `wg`. Watch-item: the app runs in a container, so
|
|
* the wg interface must be reachable from here (host network / mounted config);
|
|
* verified on the real VM, not in the mocked test-suite.
|
|
*/
|
|
class LocalWireguardHub implements WireguardHub
|
|
{
|
|
public function allocateIp(): string
|
|
{
|
|
// Both tables hand out addresses from the same subnet — an operator VPN
|
|
// access and a host peer would otherwise be given the same tunnel IP.
|
|
$used = array_flip(array_merge(
|
|
Host::query()->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 (string) config('provisioning.wireguard.endpoint');
|
|
}
|
|
|
|
public function publicKey(): string
|
|
{
|
|
return (string) config('provisioning.wireguard.hub_public_key');
|
|
}
|
|
}
|