feat: add firewall logs functionality to Proxmox Firewall component, enabling users to view recent firewall activity and enhance monitoring capabilities
This commit is contained in:
@@ -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([
|
||||
|
||||
@@ -129,22 +129,23 @@
|
||||
@if($adapter['mac'])
|
||||
<div>MAC: <code class="text-xs">{{ $adapter['mac'] }}</code></div>
|
||||
@endif
|
||||
@if($adapter['bridge'])
|
||||
<div>Bridge: <code class="text-xs">{{ $adapter['bridge'] }}</code></div>
|
||||
@endif
|
||||
</div>
|
||||
<p class="text-xs text-base/50 mt-2">Enables or disables the firewall ability for this network adapter</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 ml-4">
|
||||
<span class="text-sm font-medium text-base/70">Disabled</span>
|
||||
<label class="firewall-toggle-switch" wire:loading.attr="disabled">
|
||||
<input
|
||||
type="checkbox"
|
||||
@if($adapterFirewallStates[$adapter['name']] ?? false) checked @endif
|
||||
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
|
||||
wire:loading.attr="disabled">
|
||||
<span class="firewall-toggle-slider"></span>
|
||||
</label>
|
||||
<span class="text-sm font-medium text-base/70">Enabled</span>
|
||||
<div class="flex flex-col items-center gap-2 ml-4">
|
||||
<span class="text-xs font-medium text-base/60">Firewall</span>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-sm font-medium text-base/70">Disabled</span>
|
||||
<label class="firewall-toggle-switch" wire:loading.attr="disabled">
|
||||
<input
|
||||
type="checkbox"
|
||||
@if($adapterFirewallStates[$adapter['name']] ?? false) checked @endif
|
||||
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
|
||||
wire:loading.attr="disabled">
|
||||
<span class="firewall-toggle-slider"></span>
|
||||
</label>
|
||||
<span class="text-sm font-medium text-base/70">Enabled</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -260,7 +261,6 @@
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Destination</label>
|
||||
<select wire:model="ruleDest" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
||||
<option value="">Any</option>
|
||||
@php
|
||||
$allIps = collect();
|
||||
if ($server->primaryIpv4) {
|
||||
@@ -290,7 +290,6 @@
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-2">Interface</label>
|
||||
<select wire:model="ruleIface" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
||||
<option value="">Any</option>
|
||||
@foreach($networkAdapters as $adapter)
|
||||
<option value="{{ $adapter['name'] }}">{{ $adapter['name'] }} ({{ ucfirst($adapter['model'] ?? 'Unknown') }})</option>
|
||||
@endforeach
|
||||
@@ -400,6 +399,75 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Firewall Logs Card -->
|
||||
<div class="bg-background border border-neutral rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10">
|
||||
<x-ri-file-list-3-line class="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">Firewall Logs</h2>
|
||||
<p class="text-sm text-base/60 mt-1">Recent firewall activity and blocked/allowed connections</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
wire:click="loadFirewallLogs"
|
||||
wire:loading.attr="disabled"
|
||||
class="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors text-sm font-medium">
|
||||
<x-ri-refresh-line class="size-4 inline mr-1" />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if(count($firewallLogs) > 0)
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b border-neutral">
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">Timestamp</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">Action</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">Source</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">Destination</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">Protocol</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">Source Port</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">Dest Port</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">TTL</th>
|
||||
<th class="text-left py-3 px-4 font-semibold text-sm">ACK</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($firewallLogs as $log)
|
||||
<tr class="border-b border-neutral/20 hover:bg-background-secondary/50">
|
||||
<td class="py-3 px-4 text-sm">{{ $log['timestamp'] ?? '-' }}</td>
|
||||
<td class="py-3 px-4 text-sm">
|
||||
<span class="px-2 py-1 rounded text-xs font-medium
|
||||
{{ ($log['action'] ?? '') === 'ACCEPT' ? 'bg-success/20 text-success' : '' }}
|
||||
{{ ($log['action'] ?? '') === 'REJECT' ? 'bg-warning/20 text-warning' : '' }}
|
||||
{{ ($log['action'] ?? '') === 'DROP' ? 'bg-error/20 text-error' : '' }}">
|
||||
{{ $log['action'] ?? '-' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="py-3 px-4 text-sm"><code class="text-xs">{{ $log['source'] ?? '-' }}</code></td>
|
||||
<td class="py-3 px-4 text-sm"><code class="text-xs">{{ $log['destination'] ?? '-' }}</code></td>
|
||||
<td class="py-3 px-4 text-sm">{{ $log['proto'] ?? '-' }}</td>
|
||||
<td class="py-3 px-4 text-sm">{{ $log['spt'] ?? '-' }}</td>
|
||||
<td class="py-3 px-4 text-sm">{{ $log['dpt'] ?? '-' }}</td>
|
||||
<td class="py-3 px-4 text-sm">{{ $log['ttl'] ?? '-' }}</td>
|
||||
<td class="py-3 px-4 text-sm">{{ $log['ack'] ?? '-' }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@else
|
||||
<div class="text-center py-8 text-base/60">
|
||||
<x-ri-file-list-3-line class="size-12 mx-auto mb-3 opacity-50" />
|
||||
<p>No firewall logs available</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-8 text-center">
|
||||
<x-ri-shield-line class="size-16 mx-auto mb-4 opacity-50" />
|
||||
|
||||
Reference in New Issue
Block a user