diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php new file mode 100644 index 000000000..8b088a9db --- /dev/null +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -0,0 +1,379 @@ +loadFirewallOptions(); + $this->loadFirewallRules(); + $this->loadAliases(); + $this->loadIpsets(); + } + + public function loadFirewallOptions(): void + { + try { + $proxmox = new Proxmox; + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options' + ); + $data = $response->json()['data'] ?? []; + $this->firewallEnabled = isset($data['enable']) && $data['enable'] == 1; + $this->error = ''; + } catch (\Exception $e) { + $this->error = 'Failed to load firewall options: ' . $e->getMessage(); + $this->firewallEnabled = false; + } + } + + public function toggleFirewall(): void + { + $this->loading = true; + try { + $proxmox = new Proxmox; + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options', + 'put', + ['enable' => $this->firewallEnabled ? 0 : 1] + ); + + if ($response->successful()) { + $this->firewallEnabled = !$this->firewallEnabled; + $this->notify('Firewall ' . ($this->firewallEnabled ? 'enabled' : 'disabled') . ' successfully', 'success'); + $this->loadFirewallOptions(); + } else { + $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $this->notify('Failed to toggle firewall: ' . $message, 'error'); + } + } catch (\Exception $e) { + $this->notify('Failed to toggle firewall: ' . $e->getMessage(), 'error'); + } finally { + $this->loading = false; + } + } + + public function loadFirewallRules(): void + { + try { + $proxmox = new Proxmox; + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules' + ); + $data = $response->json()['data'] ?? []; + $this->rules = is_array($data) ? $data : []; + $this->error = ''; + } catch (\Exception $e) { + $this->error = 'Failed to load firewall rules: ' . $e->getMessage(); + $this->rules = []; + } + } + + public function startCreateRule(): void + { + $this->resetRuleForm(); + $this->editingRulePos = null; + } + + public function startEditRule(int $pos): void + { + $rule = collect($this->rules)->firstWhere('pos', $pos); + if ($rule) { + $this->editingRulePos = $pos; + $this->ruleAction = $rule['action'] ?? 'ACCEPT'; + $this->ruleSource = $rule['source'] ?? ''; + $this->ruleDest = $rule['dest'] ?? ''; + $this->ruleProto = $rule['proto'] ?? ''; + $this->ruleDport = $rule['dport'] ?? ''; + $this->ruleSport = $rule['sport'] ?? ''; + $this->ruleComment = $rule['comment'] ?? ''; + $this->ruleEnable = !isset($rule['enable']) || $rule['enable'] == 1; + $this->ruleMacro = $rule['macro'] ?? ''; + $this->ruleIface = $rule['iface'] ?? ''; + $this->ruleLog = $rule['log'] ?? ''; + } + } + + public function resetRuleForm(): void + { + $this->ruleAction = 'ACCEPT'; + $this->ruleSource = ''; + $this->ruleDest = ''; + $this->ruleProto = ''; + $this->ruleDport = ''; + $this->ruleSport = ''; + $this->ruleComment = ''; + $this->ruleEnable = true; + $this->ruleMacro = ''; + $this->ruleIface = ''; + $this->ruleLog = ''; + } + + public function saveRule(): void + { + $this->loading = true; + try { + $proxmox = new Proxmox; + $payload = array_filter([ + 'action' => $this->ruleAction, + 'source' => $this->ruleSource ?: null, + 'dest' => $this->ruleDest ?: null, + 'proto' => $this->ruleProto ?: null, + 'dport' => $this->ruleDport ?: null, + 'sport' => $this->ruleSport ?: null, + 'comment' => $this->ruleComment ?: null, + 'enable' => $this->ruleEnable ? 1 : 0, + 'macro' => $this->ruleMacro ?: null, + 'iface' => $this->ruleIface ?: null, + 'log' => $this->ruleLog ?: null, + ], fn($value) => $value !== null); + + if ($this->editingRulePos !== null) { + // Update existing rule + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $this->editingRulePos, + 'put', + $payload + ); + } else { + // Create new rule + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules', + 'post', + $payload + ); + } + + if ($response->successful()) { + $this->notify('Rule ' . ($this->editingRulePos !== null ? 'updated' : 'created') . ' successfully', 'success'); + $this->resetRuleForm(); + $this->editingRulePos = null; + $this->loadFirewallRules(); + } else { + $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $this->notify('Failed to save rule: ' . $message, 'error'); + } + } catch (\Exception $e) { + $this->notify('Failed to save rule: ' . $e->getMessage(), 'error'); + } finally { + $this->loading = false; + } + } + + public function deleteRule(int $pos): void + { + $this->loading = true; + try { + $proxmox = new Proxmox; + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, + 'delete' + ); + + if ($response->successful()) { + $this->notify('Rule deleted successfully', 'success'); + $this->loadFirewallRules(); + } else { + $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $this->notify('Failed to delete rule: ' . $message, 'error'); + } + } catch (\Exception $e) { + $this->notify('Failed to delete rule: ' . $e->getMessage(), 'error'); + } finally { + $this->loading = false; + } + } + + public function loadAliases(): void + { + try { + $proxmox = new Proxmox; + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases' + ); + $data = $response->json()['data'] ?? []; + $this->aliases = is_array($data) ? $data : []; + } catch (\Exception $e) { + $this->aliases = []; + } + } + + public function createAlias(): void + { + if (empty($this->aliasName) || empty($this->aliasCidr)) { + $this->notify('Name and CIDR are required', 'error'); + return; + } + + $this->loading = true; + try { + $proxmox = new Proxmox; + $payload = [ + 'name' => $this->aliasName, + 'cidr' => $this->aliasCidr, + ]; + if ($this->aliasComment) { + $payload['comment'] = $this->aliasComment; + } + + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases', + 'post', + $payload + ); + + if ($response->successful()) { + $this->notify('Alias created successfully', 'success'); + $this->aliasName = ''; + $this->aliasCidr = ''; + $this->aliasComment = ''; + $this->loadAliases(); + } else { + $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $this->notify('Failed to create alias: ' . $message, 'error'); + } + } catch (\Exception $e) { + $this->notify('Failed to create alias: ' . $e->getMessage(), 'error'); + } finally { + $this->loading = false; + } + } + + public function deleteAlias(string $name): void + { + $this->loading = true; + try { + $proxmox = new Proxmox; + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases/' . $name, + 'delete' + ); + + if ($response->successful()) { + $this->notify('Alias deleted successfully', 'success'); + $this->loadAliases(); + } else { + $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $this->notify('Failed to delete alias: ' . $message, 'error'); + } + } catch (\Exception $e) { + $this->notify('Failed to delete alias: ' . $e->getMessage(), 'error'); + } finally { + $this->loading = false; + } + } + + public function loadIpsets(): void + { + try { + $proxmox = new Proxmox; + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset' + ); + $data = $response->json()['data'] ?? []; + $this->ipsets = is_array($data) ? $data : []; + } catch (\Exception $e) { + $this->ipsets = []; + } + } + + public function createIpset(): void + { + if (empty($this->ipsetName)) { + $this->notify('Name is required', 'error'); + return; + } + + $this->loading = true; + try { + $proxmox = new Proxmox; + $payload = [ + 'name' => $this->ipsetName, + ]; + if ($this->ipsetComment) { + $payload['comment'] = $this->ipsetComment; + } + + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset', + 'post', + $payload + ); + + if ($response->successful()) { + $this->notify('IP set created successfully', 'success'); + $this->ipsetName = ''; + $this->ipsetComment = ''; + $this->loadIpsets(); + } else { + $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $this->notify('Failed to create IP set: ' . $message, 'error'); + } + } catch (\Exception $e) { + $this->notify('Failed to create IP set: ' . $e->getMessage(), 'error'); + } finally { + $this->loading = false; + } + } + + public function deleteIpset(string $name): void + { + $this->loading = true; + try { + $proxmox = new Proxmox; + $response = $proxmox->request( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $name, + 'delete' + ); + + if ($response->successful()) { + $this->notify('IP set deleted successfully', 'success'); + $this->loadIpsets(); + } else { + $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; + $this->notify('Failed to delete IP set: ' . $message, 'error'); + } + } catch (\Exception $e) { + $this->notify('Failed to delete IP set: ' . $e->getMessage(), 'error'); + } finally { + $this->loading = false; + } + } +} + diff --git a/extensions/Servers/Proxmox/Proxmox.php b/extensions/Servers/Proxmox/Proxmox.php index 30260e797..2c4a3b0da 100644 --- a/extensions/Servers/Proxmox/Proxmox.php +++ b/extensions/Servers/Proxmox/Proxmox.php @@ -36,6 +36,7 @@ class Proxmox extends ClassServer View::addNamespace('proxmox', __DIR__ . '/views'); \Livewire\Livewire::component('proxmox.overview', \Paymenter\Extensions\Servers\Proxmox\Livewire\Overview::class); + \Livewire\Livewire::component('proxmox.firewall', \Paymenter\Extensions\Servers\Proxmox\Livewire\Firewall::class); \Livewire\Livewire::component('proxmox.network', \Paymenter\Extensions\Servers\Proxmox\Livewire\Network::class); \Livewire\Livewire::component('proxmox.index', \Paymenter\Extensions\Servers\Proxmox\Livewire\Index::class); \Livewire\Livewire::component('proxmox.server', \Paymenter\Extensions\Servers\Proxmox\Livewire\Server::class); diff --git a/extensions/Servers/Proxmox/routes.php b/extensions/Servers/Proxmox/routes.php index 196695fa7..675cd5f70 100644 --- a/extensions/Servers/Proxmox/routes.php +++ b/extensions/Servers/Proxmox/routes.php @@ -7,6 +7,7 @@ use Paymenter\Extensions\Servers\Proxmox\Livewire\CloudInit; use Paymenter\Extensions\Servers\Proxmox\Livewire\Display; use Paymenter\Extensions\Servers\Proxmox\Livewire\Index; use Paymenter\Extensions\Servers\Proxmox\Http\Controllers\LiveTrafficController; +use Paymenter\Extensions\Servers\Proxmox\Livewire\Firewall; use Paymenter\Extensions\Servers\Proxmox\Livewire\Network; use Paymenter\Extensions\Servers\Proxmox\Livewire\Overview; use Paymenter\Extensions\Servers\Proxmox\Livewire\Subdomain; @@ -23,6 +24,7 @@ Route::group([ ], function () { Route::get('/', Index::class)->name('index'); Route::get('{server}', Overview::class)->name('server.overview'); + Route::get('{server}/firewall', Firewall::class)->name('server.firewall'); Route::get('{server}/network', Network::class)->name('server.network'); Route::get('{server}/live-traffic', LiveTrafficController::class)->name('server.live-traffic'); Route::get('{server}/boot-order', BootOrder::class)->name('server.boot-order'); diff --git a/extensions/Servers/Proxmox/views/components/layout.blade.php b/extensions/Servers/Proxmox/views/components/layout.blade.php index 92aa1a734..cb1c1ff4c 100644 --- a/extensions/Servers/Proxmox/views/components/layout.blade.php +++ b/extensions/Servers/Proxmox/views/components/layout.blade.php @@ -153,6 +153,7 @@ ['label' => 'Overview', 'view' => 'overview', 'route' => route('proxmox.server.overview', ['server' => $server->id])], ['label' => 'Statistics', 'view' => 'statistics', 'route' => route('proxmox.server.statistics', ['server' => $server->id]), 'advanced' => true], ['label' => 'Boot Settings', 'view' => 'boot-order', 'route' => route('proxmox.server.boot-order', ['server' => $server->id]), 'advanced' => true], + ['label' => 'Firewall', 'view' => 'firewall', 'route' => route('proxmox.server.firewall', ['server' => $server->id])], ['label' => 'Network', 'view' => 'network', 'route' => route('proxmox.server.network', ['server' => $server->id])], ['label' => 'Subdomain', 'view' => 'subdomain', 'route' => route('proxmox.server.subdomain', ['server' => $server->id]), 'advanced' => true], ['label' => 'Cloud-init', 'view' => 'cloud-init', 'route' => route('proxmox.server.cloud-init', ['server' => $server->id]), 'advanced' => true], diff --git a/extensions/Servers/Proxmox/views/firewall.blade.php b/extensions/Servers/Proxmox/views/firewall.blade.php new file mode 100644 index 000000000..3bdc50b9a --- /dev/null +++ b/extensions/Servers/Proxmox/views/firewall.blade.php @@ -0,0 +1,366 @@ + + +
+ +
+
+
+
+ +
+
+

Firewall Status

+

Enable or disable the firewall for this virtual machine

+
+
+ +
+ @if($error) +
+

{{ $error }}

+
+ @endif +
+ + @if($firewallEnabled) + +
+
+
+
+ +
+
+

Firewall Rules

+

Manage allow and block rules for network traffic

+
+
+ +
+ + + @if($editingRulePos !== null || (empty($rules) && !$editingRulePos)) +
+
+

{{ $editingRulePos !== null ? 'Edit Rule' : 'Create New Rule' }}

+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+ @endif + + + @if(count($rules) > 0) +
+ + + + + + + + + + + + + + + @foreach($rules as $rule) + + + + + + + + + + + @endforeach + +
PositionActionSourceDestinationProtocolPortStatusActions
{{ $rule['pos'] ?? '-' }} + + {{ $rule['action'] ?? '-' }} + + {{ $rule['source'] ?? '-' }}{{ $rule['dest'] ?? '-' }}{{ strtoupper($rule['proto'] ?? 'any') }}{{ $rule['dport'] ?? ($rule['sport'] ?? '-') }} + @if(isset($rule['enable']) && $rule['enable'] == 0) + Disabled + @else + Enabled + @endif + +
+ + +
+
+
+ @else +
+ +

No firewall rules configured

+
+ @endif +
+ + +
+
+
+
+ +
+
+

Aliases

+

Create named aliases for IP addresses and networks

+
+
+
+ + +
+

Create New Alias

+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + @if(count($aliases) > 0) +
+ + + + + + + + + + + @foreach($aliases as $alias) + + + + + + + @endforeach + +
NameCIDRCommentActions
{{ $alias['name'] ?? '-' }}{{ $alias['cidr'] ?? '-' }}{{ $alias['comment'] ?? '-' }} + +
+
+ @else +
+ +

No aliases configured

+
+ @endif +
+ + +
+
+
+
+ +
+
+

IP Sets

+

Manage sets of IP addresses for use in firewall rules

+
+
+
+ + +
+

Create New IP Set

+
+
+ + +
+
+ + +
+
+ +
+ + + @if(count($ipsets) > 0) +
+ + + + + + + + + + @foreach($ipsets as $ipset) + + + + + + @endforeach + +
NameCommentActions
{{ $ipset['name'] ?? '-' }}{{ $ipset['comment'] ?? '-' }} + +
+
+ @else +
+ +

No IP sets configured

+
+ @endif +
+ @else +
+ +

Firewall is Disabled

+

Enable the firewall above to manage rules, aliases, and IP sets.

+
+ @endif +
+ +
+ diff --git a/extensions/Servers/Proxmox/views/help.blade.php b/extensions/Servers/Proxmox/views/help.blade.php index bfd430aba..7b99668ab 100644 --- a/extensions/Servers/Proxmox/views/help.blade.php +++ b/extensions/Servers/Proxmox/views/help.blade.php @@ -129,6 +129,13 @@ Boot Settings +