105 lines
3.8 KiB
PHP
105 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Provisioning\Jobs;
|
|
|
|
use App\Models\Host;
|
|
use App\Models\VpnPeer;
|
|
use App\Services\Wireguard\WireguardHub;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Foundation\Bus\Dispatchable;
|
|
use Illuminate\Queue\InteractsWithQueue;
|
|
use Illuminate\Queue\SerializesModels;
|
|
|
|
/**
|
|
* Copy the hub's live peer state into the database so the console can show it.
|
|
*
|
|
* The console runs in the web container, which has no wg0 — only this worker
|
|
* can read `wg show`. Everything the operator sees (handshakes, traffic, who is
|
|
* connected) therefore travels through this job.
|
|
*
|
|
* Reconciles in both directions: peers found on the interface are upserted
|
|
* (adopting host peers the provisioning pipeline registered, so the list has no
|
|
* blind spots), and rows no longer on the interface are marked absent rather
|
|
* than deleted — a missing peer is information, not a reason to forget it.
|
|
*/
|
|
class SyncVpnPeers implements ShouldBeUnique, ShouldQueue
|
|
{
|
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
|
|
/** Long enough that a stuck job cannot block syncing forever. */
|
|
public int $uniqueFor = 120;
|
|
|
|
public int $tries = 1;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->onQueue('provisioning');
|
|
}
|
|
|
|
public function handle(WireguardHub $hub): void
|
|
{
|
|
$snapshots = $hub->peers();
|
|
$now = now();
|
|
|
|
// hosts.wg_pubkey => host id, so pipeline-created peers show up
|
|
// under their host name instead of as an unknown key.
|
|
$hostsByKey = Host::query()
|
|
->whereNotNull('wg_pubkey')
|
|
->pluck('id', 'wg_pubkey');
|
|
|
|
foreach ($snapshots as $publicKey => $snapshot) {
|
|
$peer = VpnPeer::withTrashed()->where('public_key', $publicKey)->first();
|
|
|
|
// Revoked, yet still on the hub: the removal never landed. Retry it
|
|
// rather than adopting the peer back into the console — otherwise a
|
|
// failed removal quietly becomes a live access again.
|
|
if ($peer?->trashed()) {
|
|
ApplyVpnPeer::dispatch($publicKey, null, false);
|
|
|
|
continue;
|
|
}
|
|
$hostId = $hostsByKey[$publicKey] ?? null;
|
|
|
|
$observed = [
|
|
'present' => true,
|
|
'endpoint' => $snapshot->endpoint,
|
|
'last_handshake_at' => $snapshot->latestHandshake,
|
|
'rx_bytes' => $snapshot->rxBytes,
|
|
'tx_bytes' => $snapshot->txBytes,
|
|
'observed_at' => $now,
|
|
];
|
|
|
|
if ($peer === null) {
|
|
VpnPeer::create($observed + [
|
|
'public_key' => $publicKey,
|
|
// Fall back to the dump's allowed-ips for peers that were
|
|
// never created through the console.
|
|
'allowed_ip' => strtok($snapshot->allowedIps, '/') ?: $snapshot->allowedIps,
|
|
'host_id' => $hostId,
|
|
'name' => $hostId !== null
|
|
? (Host::find($hostId)?->name ?? __('vpn.unknown_peer'))
|
|
: __('vpn.unknown_peer'),
|
|
'enabled' => true,
|
|
]);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Never overwrite `enabled`: that is the operator's intent, and a
|
|
// peer being present only says the hub has not caught up yet.
|
|
$peer->update($observed + ['host_id' => $peer->host_id ?? $hostId]);
|
|
}
|
|
|
|
$seen = array_keys($snapshots);
|
|
|
|
VpnPeer::query()
|
|
->whereNotIn('public_key', $seen)
|
|
->update(['present' => false, 'observed_at' => $now]);
|
|
|
|
// A tombstone whose peer is gone from the hub has done its job.
|
|
VpnPeer::onlyTrashed()->whereNotIn('public_key', $seen)->forceDelete();
|
|
}
|
|
}
|