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 bool $showRuleForm = false;
|
||||||
public array $networkAdapters = [];
|
public array $networkAdapters = [];
|
||||||
public array $adapterFirewallStates = [];
|
public array $adapterFirewallStates = [];
|
||||||
|
public array $firewallLogs = [];
|
||||||
|
public int $logLimit = 100;
|
||||||
|
|
||||||
// Rule creation/editing
|
// Rule creation/editing
|
||||||
public ?int $editingRulePos = null;
|
public ?int $editingRulePos = null;
|
||||||
@@ -52,6 +54,7 @@ class Firewall extends Component
|
|||||||
$this->loadAliases();
|
$this->loadAliases();
|
||||||
$this->loadIpsets();
|
$this->loadIpsets();
|
||||||
$this->loadNetworkAdapters();
|
$this->loadNetworkAdapters();
|
||||||
|
$this->loadFirewallLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,6 +195,28 @@ class Firewall extends Component
|
|||||||
public function startCreateRule(): void
|
public function startCreateRule(): void
|
||||||
{
|
{
|
||||||
$this->resetRuleForm();
|
$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->editingRulePos = null;
|
||||||
$this->showRuleForm = true;
|
$this->showRuleForm = true;
|
||||||
}
|
}
|
||||||
@@ -248,6 +273,7 @@ class Firewall extends Component
|
|||||||
if (!empty(trim($this->ruleSource))) {
|
if (!empty(trim($this->ruleSource))) {
|
||||||
$payload['source'] = trim($this->ruleSource);
|
$payload['source'] = trim($this->ruleSource);
|
||||||
}
|
}
|
||||||
|
// Destination is required, always include it
|
||||||
if (!empty(trim($this->ruleDest))) {
|
if (!empty(trim($this->ruleDest))) {
|
||||||
$payload['dest'] = trim($this->ruleDest);
|
$payload['dest'] = trim($this->ruleDest);
|
||||||
}
|
}
|
||||||
@@ -266,6 +292,7 @@ class Firewall extends Component
|
|||||||
if (!empty(trim($this->ruleMacro))) {
|
if (!empty(trim($this->ruleMacro))) {
|
||||||
$payload['macro'] = trim($this->ruleMacro);
|
$payload['macro'] = trim($this->ruleMacro);
|
||||||
}
|
}
|
||||||
|
// Interface is required, always include it
|
||||||
if (!empty(trim($this->ruleIface))) {
|
if (!empty(trim($this->ruleIface))) {
|
||||||
$payload['iface'] = 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()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('proxmox::firewall')->layoutData([
|
return view('proxmox::firewall')->layoutData([
|
||||||
|
|||||||
@@ -129,22 +129,23 @@
|
|||||||
@if($adapter['mac'])
|
@if($adapter['mac'])
|
||||||
<div>MAC: <code class="text-xs">{{ $adapter['mac'] }}</code></div>
|
<div>MAC: <code class="text-xs">{{ $adapter['mac'] }}</code></div>
|
||||||
@endif
|
@endif
|
||||||
@if($adapter['bridge'])
|
|
||||||
<div>Bridge: <code class="text-xs">{{ $adapter['bridge'] }}</code></div>
|
|
||||||
@endif
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-xs text-base/50 mt-2">Enables or disables the firewall ability for this network adapter</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 ml-4">
|
<div class="flex flex-col items-center gap-2 ml-4">
|
||||||
<span class="text-sm font-medium text-base/70">Disabled</span>
|
<span class="text-xs font-medium text-base/60">Firewall</span>
|
||||||
<label class="firewall-toggle-switch" wire:loading.attr="disabled">
|
<div class="flex items-center gap-3">
|
||||||
<input
|
<span class="text-sm font-medium text-base/70">Disabled</span>
|
||||||
type="checkbox"
|
<label class="firewall-toggle-switch" wire:loading.attr="disabled">
|
||||||
@if($adapterFirewallStates[$adapter['name']] ?? false) checked @endif
|
<input
|
||||||
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
|
type="checkbox"
|
||||||
wire:loading.attr="disabled">
|
@if($adapterFirewallStates[$adapter['name']] ?? false) checked @endif
|
||||||
<span class="firewall-toggle-slider"></span>
|
wire:click="toggleAdapterFirewall('{{ $adapter['name'] }}')"
|
||||||
</label>
|
wire:loading.attr="disabled">
|
||||||
<span class="text-sm font-medium text-base/70">Enabled</span>
|
<span class="firewall-toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
<span class="text-sm font-medium text-base/70">Enabled</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,7 +261,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-2">Destination</label>
|
<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">
|
<select wire:model="ruleDest" class="w-full px-3 py-2 border border-neutral rounded-lg bg-background">
|
||||||
<option value="">Any</option>
|
|
||||||
@php
|
@php
|
||||||
$allIps = collect();
|
$allIps = collect();
|
||||||
if ($server->primaryIpv4) {
|
if ($server->primaryIpv4) {
|
||||||
@@ -290,7 +290,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-2">Interface</label>
|
<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">
|
<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)
|
@foreach($networkAdapters as $adapter)
|
||||||
<option value="{{ $adapter['name'] }}">{{ $adapter['name'] }} ({{ ucfirst($adapter['model'] ?? 'Unknown') }})</option>
|
<option value="{{ $adapter['name'] }}">{{ $adapter['name'] }} ({{ ucfirst($adapter['model'] ?? 'Unknown') }})</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
@@ -400,6 +399,75 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</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
|
@else
|
||||||
<div class="bg-background-secondary border border-neutral/20 rounded-lg p-8 text-center">
|
<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" />
|
<x-ri-shield-line class="size-16 mx-auto mb-4 opacity-50" />
|
||||||
|
|||||||
Reference in New Issue
Block a user