Private
Public Access
1
0

refactor: simplify Proxmox Firewall log handling by removing unnecessary log grouping and optimizing log fetching logic

This commit is contained in:
2025-12-02 16:54:50 -05:00
parent 16be121e8c
commit 3f28a92ece
2 changed files with 25 additions and 975 deletions

View File

@@ -21,7 +21,7 @@ class Firewall extends Component
public array $networkAdapters = [];
public array $adapterFirewallStates = [];
public array $firewallLogs = [];
public int $logLimit = 500;
public int $logLimit = 100;
public array $availableIps = [];
public bool $showDeleteModal = false;
public ?int $deletingRulePos = null;
@@ -148,11 +148,6 @@ class Firewall extends Component
$newState = $currentState ? 0 : 1;
$payload = ['enable' => $newState];
// If enabling firewall, also disable DHCP
if ($newState === 1) {
$payload['dhcp'] = 0;
}
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options',
'put',
@@ -807,10 +802,7 @@ class Firewall extends Component
$response = $this->firewallRequest(
'/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log',
'get',
[
'limit' => $this->logLimit,
'start' => 0
]
['limit' => $this->logLimit]
);
if (! $response->successful()) {
@@ -832,50 +824,17 @@ class Firewall extends Component
return;
}
// Proxmox firewall log endpoint returns text/plain format
// Each log entry is on a new line
$body = $response->body();
$rawLogs = [];
$contentType = strtolower($response->header('Content-Type', ''));
if (! empty($body)) {
if (str_contains($contentType, 'application/json')) {
// When Accept header allows JSON, Proxmox returns structured data
$jsonData = $response->json();
$entries = [];
if (is_array($jsonData)) {
$entries = $jsonData['data'] ?? $jsonData;
}
if (is_array($entries)) {
foreach ($entries as $entry) {
$logText = null;
if (is_array($entry)) {
$logText = $entry['log']
?? $entry['text']
?? $entry['t']
?? $entry['n']
?? null;
} elseif (is_string($entry)) {
$logText = $entry;
}
if (is_string($logText) && trim($logText) !== '') {
$rawLogs[] = trim($logText);
}
}
}
} else {
// Proxmox firewall log endpoint may also return text/plain
// Each log entry is on a new line (handle both \n and \r\n)
$lines = preg_split('/\r\n|\r|\n/', $body);
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line !== '') {
$rawLogs[] = $line;
}
}
}
// Split by newlines and filter empty lines
$lines = explode("\n", trim($body));
$rawLogs = array_filter($lines, static function ($line) {
return ! empty(trim($line));
});
}
\Log::debug('Firewall logs fetched from Proxmox API', [
@@ -883,12 +842,11 @@ class Firewall extends Component
'body_length' => strlen($body),
'content_type' => $response->header('Content-Type'),
'sample' => ! empty($rawLogs) ? substr($rawLogs[array_key_first($rawLogs)], 0, 200) : null,
'first_5_lines' => array_slice($rawLogs, 0, 5),
]);
$this->firewallLogs = [];
foreach ($rawLogs as $index => $logEntry) {
foreach ($rawLogs as $logEntry) {
$logText = '';
if (is_string($logEntry)) {
@@ -910,36 +868,14 @@ class Firewall extends Component
continue;
}
$parsedLog = $this->parseFirewallLog($logText);
$this->firewallLogs[] = $parsedLog;
// Debug first few parsed logs
if ($index < 3) {
\Log::debug('Parsed firewall log entry', [
'index' => $index,
'raw' => substr($logText, 0, 200),
'parsed' => $parsedLog,
]);
}
$this->firewallLogs[] = $this->parseFirewallLog($logText);
}
// Group similar logs
$this->firewallLogs = $this->groupSimilarLogs($this->firewallLogs);
\Log::debug('Firewall logs processing complete', [
'raw_logs_count' => count($rawLogs),
'parsed_logs_count' => count($this->firewallLogs),
'sample_parsed_logs' => array_slice($this->firewallLogs, 0, 3),
]);
if (empty($this->firewallLogs) && ! empty($rawLogs)) {
$this->logError = 'Logs received but could not be processed.';
} elseif (empty($this->firewallLogs)) {
$this->logError = 'No firewall logs available. Ensure firewall logging is enabled in Proxmox (Datacenter > Firewall > Options).';
}
// Dispatch event to update frontend without full re-render
$this->dispatch('logs-updated', logs: $this->firewallLogs);
} catch (\Exception $e) {
$errorMessage = $e->getMessage();
if (method_exists($e, 'response') && $e->response()) {
@@ -963,76 +899,6 @@ class Firewall extends Component
}
}
/**
* Group similar log entries together
*/
protected function groupSimilarLogs(array $logs): array
{
if (empty($logs)) {
return [];
}
$grouped = [];
$groups = [];
foreach ($logs as $log) {
// Create a signature for grouping: action + protocol + source + destination + dest_port
$signature = sprintf(
'%s|%s|%s|%s|%s',
$log['action'] ?? '',
$log['protocol'] ?? '',
$log['source'] ?? '',
$log['destination'] ?? '',
$log['destination_port'] ?? ''
);
if (!isset($groups[$signature])) {
$groups[$signature] = [
'signature' => $signature,
'count' => 0,
'first_log' => $log,
'last_timestamp' => $log['timestamp_iso'] ?? null,
'first_timestamp' => $log['timestamp_iso'] ?? null,
'logs' => [],
];
}
$groups[$signature]['count']++;
$groups[$signature]['logs'][] = $log;
// Update timestamps
if ($log['timestamp_iso']) {
if (!$groups[$signature]['first_timestamp'] || $log['timestamp_iso'] < $groups[$signature]['first_timestamp']) {
$groups[$signature]['first_timestamp'] = $log['timestamp_iso'];
}
if (!$groups[$signature]['last_timestamp'] || $log['timestamp_iso'] > $groups[$signature]['last_timestamp']) {
$groups[$signature]['last_timestamp'] = $log['timestamp_iso'];
}
}
}
// Convert groups to array and add group metadata
foreach ($groups as $group) {
$firstLog = $group['first_log'];
$firstLog['is_group'] = $group['count'] > 1;
$firstLog['group_count'] = $group['count'];
$firstLog['grouped_logs'] = $group['logs'];
$firstLog['group_first_timestamp'] = $group['first_timestamp'];
$firstLog['group_last_timestamp'] = $group['last_timestamp'];
$grouped[] = $firstLog;
}
// Sort by most recent timestamp
usort($grouped, function($a, $b) {
$aTime = $a['group_last_timestamp'] ?? $a['timestamp_iso'] ?? '';
$bTime = $b['group_last_timestamp'] ?? $b['timestamp_iso'] ?? '';
return strcmp($bTime, $aTime); // Descending order (most recent first)
});
return $grouped;
}
protected function parseFirewallLog(string $logText): array
{
$result = [
@@ -1135,101 +1001,6 @@ class Firewall extends Component
return $result;
}
/**
* Check if an IP address is public (callable from frontend)
*/
public function checkIsPublicIP(string $ip): bool
{
return $this->isPublicIP($ip);
}
/**
* Check if an IP address is public (not private/localhost)
*/
protected function isPublicIP(string $ip): bool
{
$ip = $this->extractIPFromString($ip);
if (empty($ip)) {
return false;
}
// Check for localhost
if (in_array(strtolower($ip), ['localhost', '127.0.0.1', '::1'])) {
return false;
}
// Validate IP format
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
return false;
}
// Check if it's a private IP
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return true;
}
return false;
}
/**
* Extract IP address from a string (removes port if present)
*/
protected function extractIPFromString(string $input): string
{
$input = trim($input);
// Handle IPv6 with port: [2001:db8::1]:8080
if (preg_match('/^\[([^\]]+)\](?::\d+)?$/', $input, $matches)) {
return $matches[1];
}
// Handle IPv4 with port: 192.168.1.1:8080
if (preg_match('/^([^:]+):\d+$/', $input, $matches)) {
// Make sure it's not IPv6 without brackets
if (!str_contains($matches[1], ':')) {
return $matches[1];
}
}
// Return as-is if no port detected
return $input;
}
/**
* Fetch IP geolocation data
*/
public function fetchIPLocation($ip): array
{
if (empty($ip)) {
return ['status' => 'fail', 'message' => 'IP address is required'];
}
// Extract and validate IP address
$cleanIP = $this->extractIPFromString((string)$ip);
if (!$this->isPublicIP($cleanIP)) {
return ['status' => 'fail', 'message' => 'Not a valid public IP address'];
}
try {
// Use HTTP protocol to fetch geolocation data
$response = \Illuminate\Support\Facades\Http::timeout(5)->get("http://ip-api.com/json/{$cleanIP}");
if ($response->successful()) {
return $response->json();
}
return ['status' => 'fail', 'message' => 'Failed to fetch location data'];
} catch (\Exception $e) {
\Log::error('Error fetching IP location', [
'ip' => $cleanIP,
'error' => $e->getMessage(),
]);
return ['status' => 'fail', 'message' => 'Error: ' . $e->getMessage()];
}
}
public function render()
{
return view('proxmox::firewall')->layoutData([