onQueue('provisioning'); } public static function for(VpnPeer $peer): self { return new self($peer->public_key, $peer->allowed_ip, $peer->enabled); } public function handle(WireguardHub $hub): void { // Shared with SyncVpnPeers so a reconciliation cannot read the hub // around this mutation and then act on what it saw. Cache::lock('wireguard:hub', 30)->block(10, fn () => $this->apply($hub)); } private function apply(WireguardHub $hub): void { // Read the CURRENT desired state rather than the one captured at // dispatch time. A retried block job would otherwise undo an unblock // that happened in the meantime, and nothing would ever put it back: // SyncVpnPeers only observes, it never re-applies intent. $peer = VpnPeer::withTrashed()->where('public_key', $this->publicKey)->first(); // A missing row can only mean the access is gone — deleted, tombstone // already purged. A stale enable payload must never be able to re-add a // key that no longer has a row to revoke it with, so the captured state // can only ever remove, never enable. $enabled = ! $this->force && $peer !== null && ! $peer->trashed() && $peer->enabled; $ip = $peer?->allowed_ip ?? $this->allowedIp; // Checked before touching the hub: a forced revocation whose key has // since been re-issued to someone else must not disconnect them. if ($this->force && $peer !== null && ! ($peer->kind === VpnPeer::KIND_SYSTEM && $peer->created_by === null && $peer->user_id === null)) { Log::info('forced revocation skipped: the key belongs to a live access again', [ 'peer' => $peer->uuid, ]); return; } if ($enabled && $ip !== null) { $hub->addPeer($this->publicKey, $ip); } else { $hub->removePeer($this->publicKey); } // Adoption artifact cleaned up; the guard above already sent anything // else home. if ($this->force && $peer !== null) { $peer->forceDelete(); return; } if ($peer === null) { return; } // The tombstone has served its purpose once the hub has dropped the peer. if ($peer->trashed()) { $peer->forceDelete(); return; } // Reflect the change immediately instead of waiting for the next sync, // so the console does not sit on "wird angewendet" for a whole minute. $peer->update(['present' => $enabled, 'observed_at' => now()]); } }