From 51b9997c288a7ac4db60f83616eac8dd478e8ca9 Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Mon, 1 Dec 2025 14:11:05 -0500 Subject: [PATCH] feat: add firewall logs functionality to Proxmox Firewall component, enabling users to view recent firewall activity and enhance monitoring capabilities --- .../Servers/Proxmox/Livewire/Firewall.php | 125 ++++++++++++++++++ .../Servers/Proxmox/views/firewall.blade.php | 100 +++++++++++--- 2 files changed, 209 insertions(+), 16 deletions(-) diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index fee4afbe9..23a54669a 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -19,6 +19,8 @@ class Firewall extends Component public bool $showRuleForm = false; public array $networkAdapters = []; public array $adapterFirewallStates = []; + public array $firewallLogs = []; + public int $logLimit = 100; // Rule creation/editing public ?int $editingRulePos = null; @@ -52,6 +54,7 @@ class Firewall extends Component $this->loadAliases(); $this->loadIpsets(); $this->loadNetworkAdapters(); + $this->loadFirewallLogs(); } /** @@ -192,6 +195,28 @@ class Firewall extends Component public function startCreateRule(): void { $this->resetRuleForm(); + + // Set default destination to first IP address + $allIps = collect(); + if ($this->server->primaryIpv4) { + $allIps->push($this->server->primaryIpv4->ip); + } + if ($this->server->primaryIpv6) { + $allIps->push($this->server->primaryIpv6->ip); + } + foreach ($this->server->ipAddresses as $ipAddress) { + $allIps->push($ipAddress->ip); + } + $uniqueIps = $allIps->unique(); + if ($uniqueIps->isNotEmpty()) { + $this->ruleDest = $uniqueIps->first(); + } + + // Set default interface to first network adapter + if (!empty($this->networkAdapters)) { + $this->ruleIface = $this->networkAdapters[0]['name']; + } + $this->editingRulePos = null; $this->showRuleForm = true; } @@ -248,6 +273,7 @@ class Firewall extends Component if (!empty(trim($this->ruleSource))) { $payload['source'] = trim($this->ruleSource); } + // Destination is required, always include it if (!empty(trim($this->ruleDest))) { $payload['dest'] = trim($this->ruleDest); } @@ -266,6 +292,7 @@ class Firewall extends Component if (!empty(trim($this->ruleMacro))) { $payload['macro'] = trim($this->ruleMacro); } + // Interface is required, always include it if (!empty(trim($this->ruleIface))) { $payload['iface'] = trim($this->ruleIface); } @@ -687,6 +714,104 @@ class Firewall extends Component } } + public function loadFirewallLogs(): void + { + try { + $response = $this->firewallRequest( + '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', + 'get', + ['limit' => $this->logLimit] + ); + $rawLogs = $response->json()['data'] ?? []; + + $this->firewallLogs = []; + foreach ($rawLogs as $logEntry) { + $logText = $logEntry['n'] ?? $logEntry['t'] ?? ''; + if (empty($logText)) { + continue; + } + + // Parse the log entry to extract simplified information + $parsed = $this->parseFirewallLog($logText); + if ($parsed) { + $this->firewallLogs[] = $parsed; + } + } + } catch (\Exception $e) { + $this->firewallLogs = []; + } + } + + protected function parseFirewallLog(string $logText): ?array + { + // Example log: 202500009 2 tap202500009i0-IN 01/Dec/2025:09:00:39 -1000 REJECT: IN=fwbr202500009i0 OUT=fwbr202500009i0 PHYSIN=fwln202500009i0 PHYSOUT=tap202500009i0 MAC=8e:ef:c1:86:d4:20:58:9c:fc:10:ff:d3:08:00 SRC=24.210.66.222 DST=45.154.99.21 LEN=52 TOS=0x00 PREC=0x00 TTL=115 ID=58510 DF PROTO=TCP SPT=50692 DPT=22 SEQ=2584199599 ACK=0 WINDOW=65535 SYN + + // Extract SRC, DST, TTL, PROTO, SPT, DPT, ACK + $result = [ + 'source' => null, + 'destination' => null, + 'ttl' => null, + 'proto' => null, + 'spt' => null, + 'dpt' => null, + 'ack' => null, + 'action' => null, + 'timestamp' => null, + ]; + + // Extract action (REJECT, ACCEPT, DROP) - usually before the colon + if (preg_match('/(REJECT|ACCEPT|DROP):/i', $logText, $matches)) { + $result['action'] = strtoupper($matches[1]); + } + + // Extract timestamp - look for date pattern like "01/Dec/2025:09:00:39" + if (preg_match('/(\d{2}\/\w{3}\/\d{4}:\d{2}:\d{2}:\d{2})/', $logText, $matches)) { + $result['timestamp'] = $matches[1]; + } + + // Extract SRC + if (preg_match('/SRC=([^\s]+)/', $logText, $matches)) { + $result['source'] = $matches[1]; + } + + // Extract DST + if (preg_match('/DST=([^\s]+)/', $logText, $matches)) { + $result['destination'] = $matches[1]; + } + + // Extract TTL + if (preg_match('/TTL=(\d+)/', $logText, $matches)) { + $result['ttl'] = (int) $matches[1]; + } + + // Extract PROTO + if (preg_match('/PROTO=(\w+)/', $logText, $matches)) { + $result['proto'] = strtoupper($matches[1]); + } + + // Extract SPT (Source Port) + if (preg_match('/SPT=(\d+)/', $logText, $matches)) { + $result['spt'] = (int) $matches[1]; + } + + // Extract DPT (Destination Port) + if (preg_match('/DPT=(\d+)/', $logText, $matches)) { + $result['dpt'] = (int) $matches[1]; + } + + // Extract ACK + if (preg_match('/ACK=(\d+)/', $logText, $matches)) { + $result['ack'] = (int) $matches[1]; + } + + // Only return if we have at least some data + if ($result['source'] || $result['destination'] || $result['proto']) { + return $result; + } + + return null; + } + public function render() { return view('proxmox::firewall')->layoutData([ diff --git a/extensions/Servers/Proxmox/views/firewall.blade.php b/extensions/Servers/Proxmox/views/firewall.blade.php index 909722186..93ed4082f 100644 --- a/extensions/Servers/Proxmox/views/firewall.blade.php +++ b/extensions/Servers/Proxmox/views/firewall.blade.php @@ -129,22 +129,23 @@ @if($adapter['mac'])
MAC: {{ $adapter['mac'] }}
@endif - @if($adapter['bridge']) -
Bridge: {{ $adapter['bridge'] }}
- @endif +

Enables or disables the firewall ability for this network adapter

-
- Disabled - - Enabled +
+ Firewall +
+ Disabled + + Enabled +
@@ -260,7 +261,6 @@
- @foreach($networkAdapters as $adapter) @endforeach @@ -400,6 +399,75 @@
@endif + + +
+
+
+
+ +
+
+

Firewall Logs

+

Recent firewall activity and blocked/allowed connections

+
+
+ +
+ + @if(count($firewallLogs) > 0) +
+ + + + + + + + + + + + + + + + @foreach($firewallLogs as $log) + + + + + + + + + + + + @endforeach + +
TimestampActionSourceDestinationProtocolSource PortDest PortTTLACK
{{ $log['timestamp'] ?? '-' }} + + {{ $log['action'] ?? '-' }} + + {{ $log['source'] ?? '-' }}{{ $log['destination'] ?? '-' }}{{ $log['proto'] ?? '-' }}{{ $log['spt'] ?? '-' }}{{ $log['dpt'] ?? '-' }}{{ $log['ttl'] ?? '-' }}{{ $log['ack'] ?? '-' }}
+
+ @else +
+ +

No firewall logs available

+
+ @endif +
@else