diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index 133452400..93a40e98c 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -14,6 +14,53 @@ use Paymenter\Extensions\Servers\Proxmox\Services\CoreRouterClient; class Firewall extends Component { + protected const RULE_TEMPLATES = [ + 'web-server' => [ + 'label' => 'Web Server Baseline', + 'description' => 'Allow HTTP/HTTPS traffic', + 'action' => 'ACCEPT', + 'type' => 'in', + 'proto' => 'tcp', + 'dport' => '80,443', + 'comment' => 'Web traffic baseline', + ], + 'ssh-only' => [ + 'label' => 'SSH Access', + 'description' => 'Allow SSH access on port 22', + 'action' => 'ACCEPT', + 'type' => 'in', + 'proto' => 'tcp', + 'dport' => '22', + 'comment' => 'SSH access', + ], + 'mail-server' => [ + 'label' => 'Mail Server', + 'description' => 'Allow SMTP/Submission/IMAP/POP3', + 'action' => 'ACCEPT', + 'type' => 'in', + 'proto' => 'tcp', + 'dport' => '25,465,587,110,995,143,993', + 'comment' => 'Mail server ports', + ], + 'dns-server' => [ + 'label' => 'DNS Server', + 'description' => 'Allow DNS queries on UDP/TCP 53', + 'action' => 'ACCEPT', + 'type' => 'in', + 'proto' => 'udp', + 'dport' => '53', + 'comment' => 'DNS queries', + ], + ]; + + protected const LOG_PRESETS = [ + 'all' => 'All Events', + 'blocked' => 'Blocked Events', + 'ssh-drops' => 'SSH Drops', + 'web-traffic' => 'Web Traffic', + 'ipv6' => 'IPv6 Traffic', + ]; + // Server & Core State public bool $firewallEnabled = false; public bool $loading = false; @@ -258,6 +305,7 @@ class Firewall extends Component public int $logLimit = 500; // Reduced to prevent 413 errors public string $logError = ''; public bool $logsLoaded = false; // Track if logs have been loaded + public string $activeLogPreset = 'all'; // Pagination for logs public int $currentPage = 1; @@ -614,6 +662,16 @@ class Firewall extends Component return self::MACRO_DEFINITIONS; } + public function getRuleTemplatesProperty(): array + { + return self::RULE_TEMPLATES; + } + + public function getLogPresetsProperty(): array + { + return self::LOG_PRESETS; + } + /** * Get details for a specific macro */ @@ -655,6 +713,60 @@ class Firewall extends Component $this->showRuleForm = true; } + public function applyRuleTemplate(string $template): void + { + $selectedTemplate = self::RULE_TEMPLATES[$template] ?? null; + if (!$selectedTemplate) { + $this->error = 'Invalid rule template selected'; + $this->showErrorModal = true; + return; + } + + $this->startCreateRule(); + $this->ruleAction = $selectedTemplate['action'] ?? 'ACCEPT'; + $this->ruleType = $selectedTemplate['type'] ?? 'in'; + $this->ruleProto = $selectedTemplate['proto'] ?? ''; + $this->ruleDport = $selectedTemplate['dport'] ?? ''; + $this->ruleComment = $selectedTemplate['comment'] ?? ''; + $this->ruleEnable = true; + $this->ruleLog = 'nolog'; + } + + public function startCreateRuleFromLog(int $filteredLogIndex): void + { + $logs = $this->getFilteredLogsProperty(); + if (!isset($logs[$filteredLogIndex])) { + $this->error = 'Could not find the selected log entry'; + $this->showErrorModal = true; + return; + } + + $log = $logs[$filteredLogIndex]; + $this->startCreateRule(); + + $action = strtoupper((string) ($log['action'] ?? 'DROP')); + $protocol = strtolower((string) ($log['protocol'] ?? '')); + $allowedProtocols = ['tcp', 'udp', 'icmp', 'igmp', 'ah', 'esp', 'ospf']; + + $source = $this->normalizeLogField($log['source'] ?? ''); + $destination = $this->normalizeLogField($log['destination'] ?? ''); + $sourcePort = $this->normalizeLogField($log['source_port'] ?? ''); + $destinationPort = $this->normalizeLogField($log['destination_port'] ?? ''); + $interface = $this->normalizeLogField($log['interface'] ?? ''); + + $this->ruleAction = in_array($action, ['ACCEPT', 'REJECT', 'DROP'], true) ? $action : 'DROP'; + $this->ruleType = 'in'; + $this->ruleSource = $source; + $this->ruleDest = $destination; + $this->ruleProto = in_array($protocol, $allowedProtocols, true) ? $protocol : ''; + $this->ruleSport = $sourcePort; + $this->ruleDport = $destinationPort; + $this->ruleIface = $interface; + $this->ruleComment = 'Created from firewall log (' . ($log['timestamp_display'] ?? ($log['timestamp'] ?? now()->toDateTimeString())) . ')'; + $this->ruleEnable = true; + $this->ruleLog = 'nolog'; + } + public function startEditRule(int $index): void { if (!isset($this->rules[$index])) { @@ -1481,21 +1593,23 @@ class Firewall extends Component public function getPaginatedLogsProperty(): array { - if (!$this->logsLoaded || empty($this->firewallLogs)) { + $filteredLogs = $this->getFilteredLogsProperty(); + if (!$this->logsLoaded || empty($filteredLogs)) { return []; } - // Logs are already sorted by timestamp (most recent first) in parseFirewallLogs + // Logs are already sorted by timestamp (most recent first) in parseFirewallLogs. $offset = ($this->currentPage - 1) * $this->perPage; - return array_slice($this->firewallLogs, $offset, $this->perPage); + return array_slice($filteredLogs, $offset, $this->perPage); } public function getTotalPagesProperty(): int { - if (!$this->logsLoaded || empty($this->firewallLogs)) { + $filteredLogs = $this->getFilteredLogsProperty(); + if (!$this->logsLoaded || empty($filteredLogs)) { return 1; } - return (int)ceil(count($this->firewallLogs) / $this->perPage); + return (int)ceil(count($filteredLogs) / $this->perPage); } public function goToPage(int $page): void @@ -1534,6 +1648,43 @@ class Firewall extends Component $this->currentPage = 1; } + public function setLogPreset(string $preset): void + { + if (!array_key_exists($preset, self::LOG_PRESETS)) { + return; + } + + $this->activeLogPreset = $preset; + $this->currentPage = 1; + } + + public function getFilteredLogsProperty(): array + { + if (!$this->logsLoaded || empty($this->firewallLogs)) { + return []; + } + + $preset = $this->activeLogPreset ?: 'all'; + if ($preset === 'all') { + return $this->firewallLogs; + } + + return array_values(array_filter($this->firewallLogs, function (array $log) use ($preset) { + $action = strtoupper((string) ($log['action'] ?? '')); + $source = strtolower((string) ($log['source'] ?? '')); + $destination = strtolower((string) ($log['destination'] ?? '')); + $destinationPort = (string) ($log['destination_port'] ?? ''); + + return match ($preset) { + 'blocked' => in_array($action, ['DROP', 'REJECT'], true), + 'ssh-drops' => in_array($action, ['DROP', 'REJECT'], true) && $destinationPort === '22', + 'web-traffic' => in_array($destinationPort, ['80', '443'], true), + 'ipv6' => str_contains($source, ':') || str_contains($destination, ':'), + default => true, + }; + })); + } + // ==================== LOG PARSING ==================== protected function parseFirewallLogs(array $rawLogs): array @@ -1623,6 +1774,20 @@ class Firewall extends Component return $parsed; } + protected function normalizeLogField(string $value): string + { + $trimmed = trim($value); + if ($trimmed === '' || $trimmed === '-') { + return ''; + } + + if (str_contains($trimmed, ',')) { + $trimmed = trim((string) explode(',', $trimmed)[0]); + } + + return $trimmed; + } + protected function convertTimestamp(string $timestamp): string { try { diff --git a/extensions/Servers/Proxmox/views/components/layout.blade.php b/extensions/Servers/Proxmox/views/components/layout.blade.php index 8664b923f..4eecadfbc 100644 --- a/extensions/Servers/Proxmox/views/components/layout.blade.php +++ b/extensions/Servers/Proxmox/views/components/layout.blade.php @@ -1613,8 +1613,36 @@ ]; } $tabs[] = ['label' => 'Help', 'view' => 'help', 'route' => route('proxmox.server.help', ['server' => $server->id]), 'advanced' => true]; + + $quickActions = [ + ['label' => 'Overview', 'icon' => 'dashboard-line', 'route' => route('proxmox.server.overview', ['server' => $server->id]), 'active' => $view === 'overview'], + ['label' => 'Network', 'icon' => 'wifi-line', 'route' => route('proxmox.server.network', ['server' => $server->id]), 'active' => $view === 'network'], + ['label' => 'Firewall', 'icon' => 'shield-line', 'route' => route('proxmox.server.firewall', ['server' => $server->id]), 'active' => $view === 'firewall'], + ['label' => 'Help', 'icon' => 'question-line', 'route' => route('proxmox.server.help', ['server' => $server->id]), 'active' => $view === 'help'], + ]; + + if ($server->service->product->settings()->where('key', 'backups')->first()?->value > 0) { + $quickActions[] = ['label' => 'Backups', 'icon' => 'save-3-line', 'route' => route('proxmox.server.backups', ['server' => $server->id]), 'active' => $view === 'backups']; + } + + if ($server->node?->vnc_enabled) { + $quickActions[] = ['label' => 'Console', 'icon' => 'terminal-box-line', 'route' => route('proxmox.server.xterm', ['server' => $server->id]), 'active' => $view === 'xterm' || $view === 'vnc']; + } @endphp +
+
+ @foreach($quickActions as $action) + + + {{ $action['label'] }} + + @endforeach +
+
+