onQueue('provisioning'); } public function handle(WireguardHub $hub): void { // Reading the hub and reconciling must not straddle a peer removal: a // snapshot taken before ApplyVpnPeer removes a peer and purges its // tombstone would otherwise be adopted afterwards as a fresh, enabled // access — restoring exactly what was revoked. ApplyVpnPeer holds the // same lock, so read and mutation are mutually exclusive. $retryRemoval = []; Cache::lock('wireguard:hub', 30)->block(10, function () use ($hub, &$retryRemoval) { $this->reconcile($hub, $retryRemoval); }); // Dispatched outside the lock: with the sync queue driver this executes // inline, and ApplyVpnPeer takes the very same lock. foreach ($retryRemoval as $publicKey) { // force: a key with no row of its own can only be revoked, never kept. ApplyVpnPeer::dispatch($publicKey, null, false, force: true); } } /** @param list $retryRemoval collected, dispatched by the caller */ private function reconcile(WireguardHub $hub, array &$retryRemoval): 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') ->get(['id', 'name', 'wg_pubkey']) ->keyBy('wg_pubkey'); foreach ($snapshots as $publicKey => $snapshot) { $peer = VpnPeer::withTrashed()->where('public_key', $publicKey)->first(); $host = $hostsByKey[$publicKey] ?? null; // 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()) { $retryRemoval[] = $publicKey; continue; } $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) { // The address already belongs to a live access: this is a key // that was just replaced and whose removal has not run yet. // Adopting it would violate the unique index and abort the whole // reconciliation — one stale key would stop every peer's state // from being updated. if (VpnPeer::query()->where('allowed_ip', $ip = strtok($snapshot->allowedIps, '/') ?: $snapshot->allowedIps)->exists()) { $retryRemoval[] = $publicKey; continue; } 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' => $ip, 'host_id' => $host?->id, // Never "staff": a staff access has an owner, and nobody is // behind a peer we merely found on the interface. 'kind' => $host !== null ? VpnPeer::KIND_HOST : VpnPeer::KIND_SYSTEM, 'name' => $host?->name ?? __('vpn.unknown_peer'), 'enabled' => true, ]); continue; } // A sync that ran between addPeer() and the step storing wg_pubkey // adopts the peer as "unknown". Once the link is known, name it // after its host — but only if no operator ever named it // (created_by is null exactly for sync-adopted rows). if ($peer->host_id === null && $host !== null) { $observed['host_id'] = $host->id; if ($peer->kind === VpnPeer::KIND_SYSTEM) { $observed['kind'] = VpnPeer::KIND_HOST; } if ($peer->created_by === null) { $observed['name'] = $host->name; } } // 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); } $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(); } }