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:
2025-12-01 14:11:05 -05:00
parent aa4115732e
commit 51b9997c28
2 changed files with 209 additions and 16 deletions

View File

@@ -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([