'None', 'Amanda' => 'Amanda Backup', 'Auth' => 'Auth (identd)', 'BGP' => 'Border Gateway Protocol', 'BitTorrent' => 'BitTorrent traffic', 'BitTorrent32' => 'BitTorrent traffic (ports 32768:65535)', 'Ceph' => 'Ceph Storage Cluster', 'CVSup' => 'CVSup', 'DHCPfwd' => 'Forwarded DHCP traffic', 'DHCPv6fwd' => 'Forwarded DHCPv6 traffic', 'DNS' => 'Domain Name System', 'Distcc' => 'Distcc', 'FTP' => 'File Transfer Protocol', 'Git' => 'Git', 'HKP' => 'OpenPGP HTTP Keyserver', 'HTTP' => 'Hypertext Transfer Protocol', 'HTTPS' => 'Hypertext Transfer Protocol (SSL)', 'ICPV2' => 'Internet Cache Protocol V2', 'IMAP' => 'Internet Message Access Protocol', 'IMAPS' => 'Internet Message Access Protocol (SSL)', 'IPsec' => 'IPsec', 'IPsecah' => 'IPsec AH', 'IPsecnat' => 'IPsec NAT-T', 'IRC' => 'Internet Relay Chat', 'Jetdirect' => 'HP Jetdirect', 'L2TP' => 'Layer 2 Tunneling Protocol', 'LDAP' => 'Lightweight Directory Access Protocol', 'LDAPS' => 'Lightweight Directory Access Protocol (SSL)', 'MSNP' => 'Microsoft Notification Protocol', 'MSSQL' => 'Microsoft SQL Server', 'Mail' => 'Mail traffic (SMTP, SMTPS, Submission)', 'NFS' => 'Network File System', 'NNTP' => 'NNTP', 'NNTPS' => 'NNTPS', 'NTP' => 'Network Time Protocol', 'OSPF' => 'OSPF', 'OpenVPN' => 'OpenVPN', 'PCA' => 'Symantec PCAnywhere', 'PMG' => 'Proxmox Mail Gateway', 'POP3' => 'POP3', 'POP3S' => 'POP3S', 'PPtP' => 'Point-to-Point Tunneling Protocol', 'Ping' => 'ICMP echo request', 'PostgreSQL' => 'PostgreSQL', 'Printer' => 'Line Printer Daemon', 'RDP' => 'Microsoft Remote Desktop', 'RIP' => 'Routing Information Protocol', 'RNDC' => 'BIND remote management', 'Razor' => 'Razor', 'Rsyncd' => 'Rsync daemon', 'SANE' => 'SANE network scanner', 'SMB' => 'Microsoft SMB', 'SMTP' => 'Simple Mail Transfer Protocol', 'SMTPS' => 'Simple Mail Transfer Protocol (SSL)', 'SNMP' => 'Simple Network Management Protocol', 'SPAMD' => 'SpamAssassin daemon traffic', 'SSH' => 'Secure Shell', 'SVN' => 'Apache Subversion', 'SixXS' => 'SixXS IPv6 Deployment', 'SpiceProxy' => 'Proxmox VE SPICE Display Proxy', 'Squid' => 'Squid web proxy', 'Submission' => 'Mail message submission', 'Syslog' => 'Syslog protocol', 'TFTP' => 'Trivial File Transfer Protocol', 'Telnet' => 'Telnet', 'Telnets' => 'Telnet (SSL)', 'Time' => 'RFC 868 Time', 'Trcrt' => 'Traceroute', 'VNC' => 'VNC traffic', 'VNCL' => 'VNC Latin1', 'Web' => 'WWW traffic (HTTP and HTTPS)', 'Webcache' => 'Web Cache/Proxy', 'Webmin' => 'Webmin', 'Whois' => 'Whois' ]; // Macro Definitions with ports and protocols (as per Proxmox) protected array $macroDefinitions = [ 'Amanda' => ['proto' => 'tcp', 'dport' => '10080,10081,10082,10083'], 'Auth' => ['proto' => 'tcp', 'dport' => '113'], 'BGP' => ['proto' => 'tcp', 'dport' => '179'], 'BitTorrent' => ['proto' => 'tcp', 'dport' => '6881:6889'], 'BitTorrent32' => ['proto' => 'tcp', 'dport' => '32768:65535'], 'Ceph' => ['proto' => 'tcp', 'dport' => '6789,6800:7300'], 'CVSup' => ['proto' => 'tcp', 'dport' => '5999'], 'DHCPfwd' => ['proto' => 'udp', 'dport' => '67:68'], 'DHCPv6fwd' => ['proto' => 'udp', 'dport' => '546:547'], 'DNS' => ['proto' => 'udp', 'dport' => '53'], 'Distcc' => ['proto' => 'tcp', 'dport' => '3632'], 'FTP' => ['proto' => 'tcp', 'dport' => '21'], 'Git' => ['proto' => 'tcp', 'dport' => '9418'], 'HKP' => ['proto' => 'tcp', 'dport' => '11371'], 'HTTP' => ['proto' => 'tcp', 'dport' => '80'], 'HTTPS' => ['proto' => 'tcp', 'dport' => '443'], 'ICPV2' => ['proto' => 'udp', 'dport' => '3130'], 'IMAP' => ['proto' => 'tcp', 'dport' => '143'], 'IMAPS' => ['proto' => 'tcp', 'dport' => '993'], 'IPsec' => ['proto' => 'udp', 'dport' => '500'], 'IPsecah' => ['proto' => 'ah', 'dport' => ''], 'IPsecnat' => ['proto' => 'udp', 'dport' => '4500'], 'IRC' => ['proto' => 'tcp', 'dport' => '6667'], 'Jetdirect' => ['proto' => 'tcp', 'dport' => '9100'], 'L2TP' => ['proto' => 'udp', 'dport' => '1701'], 'LDAP' => ['proto' => 'tcp', 'dport' => '389'], 'LDAPS' => ['proto' => 'tcp', 'dport' => '636'], 'MSNP' => ['proto' => 'tcp', 'dport' => '1863'], 'MSSQL' => ['proto' => 'tcp', 'dport' => '1433'], 'Mail' => ['proto' => 'tcp', 'dport' => '25,465,587'], 'NFS' => ['proto' => 'tcp', 'dport' => '111,2049'], 'NNTP' => ['proto' => 'tcp', 'dport' => '119'], 'NNTPS' => ['proto' => 'tcp', 'dport' => '563'], 'NTP' => ['proto' => 'udp', 'dport' => '123'], 'OSPF' => ['proto' => 'ospf', 'dport' => ''], 'OpenVPN' => ['proto' => 'udp', 'dport' => '1194'], 'PCA' => ['proto' => 'tcp', 'dport' => '5631'], 'PMG' => ['proto' => 'tcp', 'dport' => '26'], 'POP3' => ['proto' => 'tcp', 'dport' => '110'], 'POP3S' => ['proto' => 'tcp', 'dport' => '995'], 'PPtP' => ['proto' => 'tcp', 'dport' => '1723'], 'Ping' => ['proto' => 'icmp', 'dport' => ''], 'PostgreSQL' => ['proto' => 'tcp', 'dport' => '5432'], 'Printer' => ['proto' => 'tcp', 'dport' => '515'], 'RDP' => ['proto' => 'tcp', 'dport' => '3389'], 'RIP' => ['proto' => 'udp', 'dport' => '520'], 'RNDC' => ['proto' => 'tcp', 'dport' => '953'], 'Razor' => ['proto' => 'tcp', 'dport' => '2703'], 'Rsyncd' => ['proto' => 'tcp', 'dport' => '873'], 'SANE' => ['proto' => 'tcp', 'dport' => '6566'], 'SMB' => ['proto' => 'tcp', 'dport' => '445'], 'SMTP' => ['proto' => 'tcp', 'dport' => '25'], 'SMTPS' => ['proto' => 'tcp', 'dport' => '465'], 'SNMP' => ['proto' => 'udp', 'dport' => '161,162'], 'SPAMD' => ['proto' => 'tcp', 'dport' => '783'], 'SSH' => ['proto' => 'tcp', 'dport' => '22'], 'SVN' => ['proto' => 'tcp', 'dport' => '3690'], 'SixXS' => ['proto' => 'tcp', 'dport' => '3874'], 'SpiceProxy' => ['proto' => 'tcp', 'dport' => '3128'], 'Squid' => ['proto' => 'tcp', 'dport' => '3128'], 'Submission' => ['proto' => 'tcp', 'dport' => '587'], 'Syslog' => ['proto' => 'udp', 'dport' => '514'], 'TFTP' => ['proto' => 'udp', 'dport' => '69'], 'Telnet' => ['proto' => 'tcp', 'dport' => '23'], 'Telnets' => ['proto' => 'tcp', 'dport' => '992'], 'Time' => ['proto' => 'tcp', 'dport' => '37'], 'Trcrt' => ['proto' => 'udp', 'dport' => '33434:33524'], 'VNC' => ['proto' => 'tcp', 'dport' => '5900:5909'], 'VNCL' => ['proto' => 'tcp', 'dport' => '5500:5509'], 'Web' => ['proto' => 'tcp', 'dport' => '80,443'], 'Webcache' => ['proto' => 'tcp', 'dport' => '3128,8080'], 'Webmin' => ['proto' => 'tcp', 'dport' => '10000'], 'Whois' => ['proto' => 'tcp', 'dport' => '43'], ]; // IP Aliases public array $aliases = []; public bool $showAliasForm = false; public ?string $editingAliasName = null; public string $aliasName = ''; public string $aliasCidr = ''; public string $aliasComment = ''; public bool $showDeleteAliasModal = false; public ?string $deletingAliasName = null; // IPSets public array $ipsets = []; public bool $showIPSetForm = false; public ?string $editingIPSetName = null; public string $ipsetName = ''; public string $ipsetComment = ''; public bool $showDeleteIPSetModal = false; public ?string $deletingIPSetName = null; // IPSet Members public array $ipsetMembers = []; public ?string $selectedIPSetName = null; public bool $showIPSetMemberForm = false; public string $memberCidr = ''; public string $memberComment = ''; public bool $memberNomatch = false; public bool $showDeleteIPSetMemberModal = false; public ?string $deletingMemberCidr = null; // Firewall Logs public array $firewallLogs = []; public int $logLimit = 500; // Reduced to prevent 413 errors public string $logError = ''; public bool $logsLoaded = false; // Track if logs have been loaded // Pagination for logs public int $currentPage = 1; public int $perPage = 50; public ?string $lastLogRefresh = null; // Available IPs for rules public array $availableIps = []; public function mount(Server $server): void { parent::mount($server); $this->server->loadMissing('ipAddresses'); $this->loadAvailableIps(); $this->loadNetworkAdapters(); $this->loadFirewallOptions(); $this->loadFirewallRules(); $this->loadAliases(); $this->loadIPSets(); // Don't load logs on mount - load lazily when logs tab is viewed } // ==================== AVAILABLE IPS ==================== protected function loadAvailableIps(): void { $allIps = collect(); if ($this->server->primaryIpv4) { $allIps->push([ 'ip' => $this->server->primaryIpv4->ip, 'type' => 'IPv4', 'label' => 'Primary IPv4' ]); } if ($this->server->primaryIpv6) { $allIps->push([ 'ip' => $this->server->primaryIpv6->ip, 'type' => 'IPv6', 'label' => 'Primary IPv6' ]); } foreach ($this->server->ipAddresses as $ipAddress) { $allIps->push([ 'ip' => $ipAddress->ip, 'type' => strtoupper($ipAddress->type), 'label' => 'Additional' ]); } $this->availableIps = $allIps->unique('ip')->values()->toArray(); } // ==================== FIREWALL API REQUEST ==================== protected function firewallRequest(string $url, string $method = 'get', array $data = []): \Illuminate\Http\Client\Response { $baseUrl = 'https://pve.virtnet.bond'; $user = 'root@pam'; $tokenId = 'firewall'; $secret = '0a439967-0a44-4b94-8906-c36b3a40cf68'; $fullUrl = rtrim($baseUrl, '/') . '/api2/json' . $url; $authHeader = 'PVEAPIToken=' . $user . '!' . $tokenId . '=' . $secret; $acceptHeader = str_contains($url, '/firewall/log') ? 'application/json, text/plain, */*' : 'application/json'; $headers = [ 'Authorization' => $authHeader, 'Accept' => $acceptHeader, ]; // Prevent caching for log requests if (str_contains($url, '/firewall/log')) { $headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'; $headers['Pragma'] = 'no-cache'; } $request = Http::withHeaders($headers)->withoutVerifying(); $response = match($method) { 'post' => $request->post($fullUrl, $data), 'put' => $request->put($fullUrl, $data), 'delete' => $request->delete($fullUrl), default => $request->get($fullUrl, $data), }; return $response; } // ==================== NETWORK ADAPTERS ==================== protected function loadNetworkAdapters(): void { try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config' ); if ($response->successful()) { $config = $response->json()['data'] ?? []; $adapters = []; foreach ($config as $key => $value) { if (preg_match('/^net(\d+)$/', $key, $matches)) { $parts = explode(',', $value); $model = explode('=', $parts[0])[0] ?? 'unknown'; $mac = null; // Store the full configuration string for later modification $this->adapterConfigs['net' . $matches[1]] = $value; foreach ($parts as $part) { if (str_contains($part, '=')) { [$k, $v] = explode('=', $part, 2); if (trim($k) === 'firewall') { $this->adapterFirewallStates['net' . $matches[1]] = (int)$v === 1; } elseif (trim($k) === 'macaddr') { $mac = $v; } } } $adapters[] = [ 'name' => 'net' . $matches[1], 'model' => $model, 'mac' => $mac, 'enabled' => $this->adapterFirewallStates['net' . $matches[1]] ?? false ]; } } $this->networkAdapters = $adapters; } } catch (\Exception $e) { Log::error('Failed to load network adapters: ' . $e->getMessage()); } } public function toggleAdapterFirewall(string $adapter): void { $this->loading = true; try { $currentState = $this->adapterFirewallStates[$adapter] ?? false; $newState = !$currentState; // Get the full network configuration string $configString = $this->adapterConfigs[$adapter] ?? ''; if (empty($configString)) { $this->error = 'Network adapter configuration not found'; $this->showErrorModal = true; return; } // Parse the configuration string and modify the firewall parameter $parts = explode(',', $configString); $firewallFound = false; foreach ($parts as $key => $part) { $part = trim($part); if (str_contains($part, '=')) { [$k, $v] = explode('=', $part, 2); if (trim($k) === 'firewall') { $parts[$key] = 'firewall=' . ($newState ? '1' : '0'); $firewallFound = true; break; } } } // If firewall parameter doesn't exist, add it if (!$firewallFound) { $parts[] = 'firewall=' . ($newState ? '1' : '0'); } // Reconstruct the configuration string $newConfigString = implode(',', $parts); // Send the full modified configuration back to Proxmox $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [$adapter => $newConfigString] ); if ($response->successful()) { $this->adapterFirewallStates[$adapter] = $newState; $this->adapterConfigs[$adapter] = $newConfigString; // Update stored config $this->notify('Adapter firewall ' . ($newState ? 'enabled' : 'disabled'), 'success'); } else { $this->error = 'Failed to toggle adapter firewall: ' . $response->body(); $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to toggle adapter firewall: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; } } // ==================== FIREWALL OPTIONS ==================== protected function loadFirewallOptions(): void { try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options' ); if ($response->successful()) { $options = $response->json()['data'] ?? []; $this->firewallEnabled = ($options['enable'] ?? 0) == 1; $this->policyIn = strtoupper($options['policy_in'] ?? 'DROP'); $this->policyOut = strtoupper($options['policy_out'] ?? 'ACCEPT'); } } catch (\Exception $e) { Log::error('Failed to load firewall options: ' . $e->getMessage()); } } public function toggleFirewall(): void { $this->loading = true; try { $newState = !$this->firewallEnabled; // Always force DHCP disabled and NDP enabled $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options', 'put', [ 'enable' => $newState ? 1 : 0, 'dhcp' => 0, // Always disabled 'ndp' => 1, // Always enabled (allows router advertisements) ] ); if ($response->successful()) { $this->firewallEnabled = $newState; $this->notify('Firewall ' . ($newState ? 'enabled' : 'disabled'), 'success'); } else { // Show detailed error message from API $errorBody = $response->json(); $errorMsg = $errorBody['errors'] ?? $response->body(); $this->error = 'Failed to toggle firewall (Status ' . $response->status() . '): ' . (is_array($errorMsg) ? json_encode($errorMsg) : $errorMsg); $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to toggle firewall: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; } } public function updatePolicies(): void { $this->loading = true; try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/options', 'put', [ 'policy_in' => $this->policyIn, 'policy_out' => $this->policyOut, 'dhcp' => 0, // Always disabled 'ndp' => 1, // Always enabled (allows router advertisements) ] ); if ($response->successful()) { $this->notify('Firewall policies updated successfully', 'success'); $this->loadFirewallOptions(); } else { $this->error = 'Failed to update firewall policies'; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to update policies: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; } } public function openPolicyInfoModal(): void { $this->showPolicyInfoModal = true; } public function closePolicyInfoModal(): void { $this->showPolicyInfoModal = false; } // ==================== RULES ==================== /** * Get macro definitions for frontend use */ public function getMacroDefinitionsProperty(): array { return $this->macroDefinitions; } /** * Get details for a specific macro */ public function getMacroDetails(string $macro): ?array { return $this->macroDefinitions[$macro] ?? null; } protected function loadFirewallRules(): void { try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules' ); if ($response->successful()) { $this->rules = $response->json()['data'] ?? []; } } catch (\Exception $e) { Log::error('Failed to load firewall rules: ' . $e->getMessage()); } } public function startCreateRule(): void { $this->resetRuleForm(); // Set default destination to first IP if (!empty($this->availableIps)) { $this->ruleDest = $this->availableIps[0]['ip']; } // Set default interface to first adapter if (!empty($this->networkAdapters)) { $this->ruleIface = $this->networkAdapters[0]['name']; } $this->editingRuleIndex = null; $this->showRuleForm = true; } public function startEditRule(int $index): void { if (!isset($this->rules[$index])) { $this->error = 'Rule not found'; $this->showErrorModal = true; return; } $rule = $this->rules[$index]; $this->editingRuleIndex = $index; $this->showRuleForm = true; $this->ruleAction = $rule['action'] ?? 'ACCEPT'; $this->ruleType = $rule['type'] ?? 'in'; $this->ruleSource = $rule['source'] ?? ''; $this->ruleDest = $rule['dest'] ?? ''; $this->ruleProto = $rule['proto'] ?? ''; $this->ruleDport = $rule['dport'] ?? ''; $this->ruleSport = $rule['sport'] ?? ''; $this->ruleComment = $rule['comment'] ?? ''; $this->ruleEnable = !isset($rule['enable']) || $rule['enable'] != 0; $this->ruleMacro = $rule['macro'] ?? ''; $this->ruleIface = $rule['iface'] ?? ''; $this->ruleLog = $rule['log'] ?? 'nolog'; } public function saveRule(): void { $this->loading = true; try { $payload = [ 'action' => $this->ruleAction, 'type' => $this->ruleType, 'enable' => $this->ruleEnable ? 1 : 0, ]; // Note: ruleMacro is only used for UI convenience to auto-populate fields // It is NOT sent to Proxmox API - only the actual proto/dport values are sent if (!empty(trim($this->ruleIface))) { $payload['iface'] = trim($this->ruleIface); } if (!empty(trim($this->ruleSource))) { $payload['source'] = trim($this->ruleSource); } if (!empty(trim($this->ruleDest))) { $payload['dest'] = trim($this->ruleDest); } if (!empty(trim($this->ruleProto))) { $payload['proto'] = trim($this->ruleProto); } if (!empty(trim($this->ruleDport))) { $payload['dport'] = trim($this->ruleDport); } if (!empty(trim($this->ruleSport))) { $payload['sport'] = trim($this->ruleSport); } if (!empty(trim($this->ruleComment))) { $payload['comment'] = trim($this->ruleComment); } if (!empty(trim($this->ruleLog)) && $this->ruleLog !== 'nolog') { $payload['log'] = $this->ruleLog; } if ($this->editingRuleIndex !== null && isset($this->rules[$this->editingRuleIndex])) { $pos = $this->rules[$this->editingRuleIndex]['pos']; $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, 'put', $payload ); } else { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules', 'post', $payload ); } if ($response->successful()) { $this->notify('Rule ' . ($this->editingRuleIndex !== null ? 'updated' : 'created') . ' successfully', 'success'); $this->resetRuleForm(); $this->editingRuleIndex = null; $this->showRuleForm = false; $this->loadFirewallRules(); } else { $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; $this->error = 'Failed to save rule: ' . $message; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to save rule: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; } } public function toggleRule(int $index): void { try { if (!isset($this->rules[$index])) { return; } $rule = $this->rules[$index]; $pos = $rule['pos']; $currentEnable = isset($rule['enable']) ? (int)$rule['enable'] : 1; $newEnable = $currentEnable == 0 ? 1 : 0; $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, 'put', ['enable' => $newEnable] ); if ($response->successful()) { $this->notify('Rule ' . ($newEnable == 1 ? 'enabled' : 'disabled'), 'success'); $this->loadFirewallRules(); } else { $this->error = 'Failed to toggle rule'; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to toggle rule: ' . $e->getMessage(); $this->showErrorModal = true; } } public function confirmDeleteRule(int $index): void { if (!isset($this->rules[$index])) { return; } $this->deletingRuleIndex = $index; $this->showDeleteRuleModal = true; } public function deleteRule(): void { if ($this->deletingRuleIndex === null || !isset($this->rules[$this->deletingRuleIndex])) { return; } $rule = $this->rules[$this->deletingRuleIndex]; $pos = $rule['pos']; $this->loading = true; $this->showDeleteRuleModal = false; try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $pos, 'delete' ); if ($response->successful()) { $this->notify('Rule deleted successfully', 'success'); $this->loadFirewallRules(); } else { $this->error = 'Failed to delete rule'; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to delete rule: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; $this->deletingRuleIndex = null; } } public function cancelDeleteRule(): void { $this->deletingRuleIndex = null; $this->showDeleteRuleModal = false; } public function reorderRules(int $fromPos, int $toPos): void { try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/rules/' . $fromPos, 'put', ['moveto' => $toPos] ); if ($response->successful()) { $this->notify('Rules reordered successfully', 'success'); $this->loadFirewallRules(); } else { $this->error = 'Failed to reorder rules'; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to reorder rules: ' . $e->getMessage(); $this->showErrorModal = true; } } protected function resetRuleForm(): void { $this->ruleAction = 'ACCEPT'; $this->ruleType = 'in'; $this->ruleSource = ''; $this->ruleDest = ''; $this->ruleProto = ''; $this->ruleDport = ''; $this->ruleSport = ''; $this->ruleComment = ''; $this->ruleEnable = true; $this->ruleMacro = ''; $this->ruleIface = ''; $this->ruleLog = 'nolog'; } public function cancelRuleForm(): void { $this->resetRuleForm(); $this->editingRuleIndex = null; $this->showRuleForm = false; } // ==================== ALIASES ==================== protected function loadAliases(): void { try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases' ); if ($response->successful()) { $this->aliases = $response->json()['data'] ?? []; } } catch (\Exception $e) { Log::error('Failed to load aliases: ' . $e->getMessage()); } } public function startCreateAlias(): void { $this->resetAliasForm(); $this->editingAliasName = null; $this->showAliasForm = true; } public function startEditAlias(string $name): void { $alias = collect($this->aliases)->firstWhere('name', $name); if ($alias) { $this->editingAliasName = $name; $this->aliasName = $alias['name']; $this->aliasCidr = $alias['cidr']; $this->aliasComment = $alias['comment'] ?? ''; $this->showAliasForm = true; } } public function saveAlias(): void { $this->validate([ 'aliasName' => 'required|string|max:64', 'aliasCidr' => 'required|string', ]); $this->loading = true; try { $payload = [ 'name' => trim($this->aliasName), 'cidr' => trim($this->aliasCidr), ]; if (!empty(trim($this->aliasComment))) { $payload['comment'] = trim($this->aliasComment); } if ($this->editingAliasName) { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases/' . $this->editingAliasName, 'put', $payload ); } else { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases', 'post', $payload ); } if ($response->successful()) { $this->notify('Alias ' . ($this->editingAliasName ? 'updated' : 'created') . ' successfully', 'success'); $this->resetAliasForm(); $this->editingAliasName = null; $this->showAliasForm = false; $this->loadAliases(); } else { $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; $this->error = 'Failed to save alias: ' . $message; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to save alias: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; } } public function confirmDeleteAlias(string $name): void { $this->deletingAliasName = $name; $this->showDeleteAliasModal = true; } public function deleteAlias(): void { if (!$this->deletingAliasName) { return; } $this->loading = true; $this->showDeleteAliasModal = false; try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/aliases/' . $this->deletingAliasName, 'delete' ); if ($response->successful()) { $this->notify('Alias deleted successfully', 'success'); $this->loadAliases(); } else { $this->error = 'Failed to delete alias'; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to delete alias: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; $this->deletingAliasName = null; } } public function cancelDeleteAlias(): void { $this->deletingAliasName = null; $this->showDeleteAliasModal = false; } protected function resetAliasForm(): void { $this->aliasName = ''; $this->aliasCidr = ''; $this->aliasComment = ''; } public function cancelAliasForm(): void { $this->resetAliasForm(); $this->editingAliasName = null; $this->showAliasForm = false; } // ==================== IPSETS ==================== protected function loadIPSets(): void { try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset' ); if ($response->successful()) { $this->ipsets = $response->json()['data'] ?? []; } } catch (\Exception $e) { Log::error('Failed to load IPSets: ' . $e->getMessage()); } } public function startCreateIPSet(): void { $this->resetIPSetForm(); $this->editingIPSetName = null; $this->showIPSetForm = true; } public function saveIPSet(): void { $this->validate([ 'ipsetName' => 'required|string|max:64', ]); $this->loading = true; try { $payload = [ 'name' => trim($this->ipsetName), ]; if (!empty(trim($this->ipsetComment))) { $payload['comment'] = trim($this->ipsetComment); } $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset', 'post', $payload ); if ($response->successful()) { $this->notify('IPSet created successfully', 'success'); $this->resetIPSetForm(); $this->showIPSetForm = false; $this->loadIPSets(); } else { $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; $this->error = 'Failed to create IPSet: ' . $message; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to create IPSet: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; } } public function confirmDeleteIPSet(string $name): void { $this->deletingIPSetName = $name; $this->showDeleteIPSetModal = true; } public function deleteIPSet(): void { if (!$this->deletingIPSetName) { return; } $this->loading = true; $this->showDeleteIPSetModal = false; try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $this->deletingIPSetName, 'delete' ); if ($response->successful()) { $this->notify('IPSet deleted successfully', 'success'); $this->loadIPSets(); // Clear selected IPSet if it was deleted if ($this->selectedIPSetName === $this->deletingIPSetName) { $this->selectedIPSetName = null; $this->ipsetMembers = []; } } else { $this->error = 'Failed to delete IPSet'; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to delete IPSet: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; $this->deletingIPSetName = null; } } public function cancelDeleteIPSet(): void { $this->deletingIPSetName = null; $this->showDeleteIPSetModal = false; } protected function resetIPSetForm(): void { $this->ipsetName = ''; $this->ipsetComment = ''; } public function cancelIPSetForm(): void { $this->resetIPSetForm(); $this->editingIPSetName = null; $this->showIPSetForm = false; } // ==================== IPSET MEMBERS ==================== public function selectIPSet(string $name): void { $this->selectedIPSetName = $name; $this->loadIPSetMembers($name); } protected function loadIPSetMembers(string $name): void { try { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $name ); if ($response->successful()) { $this->ipsetMembers = $response->json()['data'] ?? []; } } catch (\Exception $e) { Log::error('Failed to load IPSet members: ' . $e->getMessage()); } } public function startAddIPSetMember(): void { $this->memberCidr = ''; $this->memberComment = ''; $this->memberNomatch = false; $this->showIPSetMemberForm = true; } public function saveIPSetMember(): void { if (!$this->selectedIPSetName) { return; } $this->validate([ 'memberCidr' => 'required|string', ]); $this->loading = true; try { $payload = [ 'cidr' => trim($this->memberCidr), ]; if (!empty(trim($this->memberComment))) { $payload['comment'] = trim($this->memberComment); } if ($this->memberNomatch) { $payload['nomatch'] = 1; } $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $this->selectedIPSetName, 'post', $payload ); if ($response->successful()) { $this->notify('IP/CIDR added to IPSet successfully', 'success'); $this->memberCidr = ''; $this->memberComment = ''; $this->memberNomatch = false; $this->showIPSetMemberForm = false; $this->loadIPSetMembers($this->selectedIPSetName); } else { $message = $response->json()['errors'][0]['message'] ?? 'Unknown error'; $this->error = 'Failed to add member: ' . $message; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to add member: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; } } public function confirmDeleteIPSetMember(string $cidr): void { $this->deletingMemberCidr = $cidr; $this->showDeleteIPSetMemberModal = true; } public function deleteIPSetMember(): void { if (!$this->selectedIPSetName || !$this->deletingMemberCidr) { return; } $this->loading = true; $this->showDeleteIPSetMemberModal = false; try { // URL encode the CIDR for the DELETE request $encodedCidr = urlencode($this->deletingMemberCidr); $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/ipset/' . $this->selectedIPSetName . '/' . $encodedCidr, 'delete' ); if ($response->successful()) { $this->notify('IP/CIDR removed from IPSet successfully', 'success'); $this->loadIPSetMembers($this->selectedIPSetName); } else { $this->error = 'Failed to remove member'; $this->showErrorModal = true; } } catch (\Exception $e) { $this->error = 'Failed to remove member: ' . $e->getMessage(); $this->showErrorModal = true; } finally { $this->loading = false; $this->deletingMemberCidr = null; } } public function cancelDeleteIPSetMember(): void { $this->deletingMemberCidr = null; $this->showDeleteIPSetMemberModal = false; } public function cancelIPSetMemberForm(): void { $this->memberCidr = ''; $this->memberComment = ''; $this->memberNomatch = false; $this->showIPSetMemberForm = false; } // ==================== FIREWALL LOGS ==================== public function loadFirewallLogs(): void { $this->logError = ''; try { // Proxmox returns logs with newest first when using start=0 // Fetch a larger batch to ensure we get the most recent logs $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', 'get', [ 'limit' => 5000, // Fetch more logs to get the recent ones 'start' => 0, // Start from the beginning (most recent) '_dc' => time() // Cache buster ] ); if (!$response->successful()) { $this->logError = 'Failed to fetch logs (HTTP ' . $response->status() . ')'; Log::warning('Firewall log API request failed', [ 'status' => $response->status(), 'server_id' => $this->server->id, 'vm_id' => $this->server->vm_id, ]); $this->firewallLogs = []; return; } // Proxmox firewall log endpoint can return JSON or text/plain $body = $response->body(); $allRawLogs = []; // Check content type $contentType = $response->header('Content-Type'); $isJson = str_contains($contentType ?? '', 'json'); if ($isJson) { try { $jsonData = $response->json(); if (isset($jsonData['data'])) { if (is_array($jsonData['data'])) { foreach ($jsonData['data'] as $item) { if (is_array($item) && isset($item['t'])) { $allRawLogs[] = $item['t']; } elseif (is_string($item)) { $allRawLogs[] = $item; } } } elseif (is_string($jsonData['data'])) { $lines = preg_split('/\r\n|\r|\n/', $jsonData['data']); $allRawLogs = array_filter($lines, fn($line) => !empty(trim($line))); } } elseif (is_array($jsonData)) { foreach ($jsonData as $item) { if (is_array($item) && isset($item['t'])) { $allRawLogs[] = $item['t']; } elseif (is_string($item)) { $allRawLogs[] = $item; } } } } catch (\Exception $e) { Log::warning('Failed to parse JSON response', ['error' => $e->getMessage()]); } } // Fall back to text/plain format if (empty($allRawLogs) && !empty($body)) { $lines = preg_split('/\r\n|\r|\n/', $body); $allRawLogs = array_filter($lines, fn($line) => !empty(trim($line))); } $allRawLogs = array_values($allRawLogs); Log::info('Firewall logs fetched from Proxmox API', [ 'raw_count' => count($allRawLogs), 'first_3_logs' => array_slice($allRawLogs, 0, 3) ]); $this->firewallLogs = $this->parseFirewallLogs($allRawLogs); // Only keep the most recent entries (limit to 500 for performance) if (count($this->firewallLogs) > 500) { $this->firewallLogs = array_slice($this->firewallLogs, 0, 500); } // Reset to page 1 when refreshing logs $this->currentPage = 1; // Mark logs as loaded $this->logsLoaded = true; // Set last refresh timestamp $this->lastLogRefresh = now()->format('Y-m-d H:i:s'); // Notify user of successful refresh $this->notify('Loaded ' . count($this->firewallLogs) . ' firewall log entries', 'success'); Log::info('Firewall log parsing summary', [ 'total_raw_logs' => count($allRawLogs), 'successfully_parsed' => count($this->firewallLogs), 'last_refresh' => $this->lastLogRefresh, 'first_log_timestamp' => $this->firewallLogs[0]['timestamp'] ?? 'none', 'last_log_timestamp' => $this->firewallLogs[count($this->firewallLogs) - 1]['timestamp'] ?? 'none', ]); } catch (\Exception $e) { $this->logError = 'Failed to load logs: ' . $e->getMessage(); Log::error('Firewall logs error: ' . $e->getMessage()); $this->firewallLogs = []; } } // ==================== LOG PAGINATION ==================== public function getPaginatedLogsProperty(): array { if (!$this->logsLoaded || empty($this->firewallLogs)) { return []; } // 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); } public function getTotalPagesProperty(): int { if (!$this->logsLoaded || empty($this->firewallLogs)) { return 1; } return (int)ceil(count($this->firewallLogs) / $this->perPage); } public function goToPage(int $page): void { $totalPages = $this->getTotalPagesProperty(); if ($page >= 1 && $page <= $totalPages) { $this->currentPage = $page; } } public function previousPage(): void { if ($this->currentPage > 1) { $this->currentPage--; } } public function nextPage(): void { if ($this->currentPage < $this->getTotalPagesProperty()) { $this->currentPage++; } } public function updatedPerPage(): void { // Reset to page 1 when changing per-page value $this->currentPage = 1; } public function clearLogs(): void { // Clear logs to reduce payload size when not viewing logs tab $this->firewallLogs = []; $this->logsLoaded = false; $this->currentPage = 1; } // ==================== LOG PARSING ==================== protected function parseFirewallLogs(array $rawLogs): array { $parsed = []; foreach ($rawLogs as $logEntry) { if (empty($logEntry) || !is_string($logEntry)) { continue; } $log = $this->parseLogLine($logEntry); if ($log) { $parsed[] = $log; } } // Sort logs by timestamp in descending order (most recent first) usort($parsed, function($a, $b) { $timeA = strtotime($a['timestamp_iso'] ?? $a['timestamp'] ?? '0'); $timeB = strtotime($b['timestamp_iso'] ?? $b['timestamp'] ?? '0'); return $timeB <=> $timeA; // Descending order }); return $parsed; } protected function parseLogLine(string $line): ?array { // Parse Proxmox firewall log format // Format: "vmid level chain timestamp action: KEY=VALUE ..." if (!preg_match('/^(\d+)\s+(\d+)\s+(\S+)\s+([^\s]+\s+[^\s]+)\s+(\w+):\s*(.*)$/', $line, $matches)) { return null; } $fields = [ 'raw' => $line, 'vmid' => $matches[1], 'level' => (int)$matches[2], 'chain' => $matches[3], 'timestamp' => $matches[4], 'action' => $matches[5], ]; // Parse key-value pairs $kvString = $matches[6]; if (preg_match_all('/(\w+)=([^\s]+)/', $kvString, $kvMatches, PREG_SET_ORDER)) { foreach ($kvMatches as $match) { $key = strtolower($match[1]); $value = $match[2]; $fields[$key] = $value; } } // Map fields to standardized names $parsed = [ 'raw' => $line, 'timestamp' => $fields['timestamp'] ?? '-', 'timestamp_iso' => $this->convertTimestamp($fields['timestamp'] ?? ''), 'timestamp_display' => $this->formatTimestampForDisplay($fields['timestamp'] ?? ''), 'action' => $fields['action'] ?? '-', 'level' => $fields['level'] ?? 0, 'level_label' => $this->getLevelLabel($fields['level'] ?? 0), 'level_badge_class' => $this->getLevelBadgeClass($fields['action'] ?? '-'), 'chain' => $fields['chain'] ?? '-', 'interface' => $fields['in'] ?? '-', 'source' => $fields['src'] ?? '-', 'destination' => $fields['dst'] ?? '-', 'source_port' => $fields['spt'] ?? '-', 'destination_port' => $fields['dpt'] ?? '-', 'protocol' => $fields['proto'] ?? '-', 'mac' => $fields['mac'] ?? '-', ]; // Debug log first entry to verify parsing static $loggedOnce = false; if (!$loggedOnce && count($fields) > 5) { Log::info('Firewall log parse sample', [ 'parsed_fields' => $fields, 'protocol_value' => $parsed['protocol'], 'source_value' => $parsed['source'], ]); $loggedOnce = true; } return $parsed; } protected function convertTimestamp(string $timestamp): string { try { $dt = Carbon::createFromFormat('d/M/Y:H:i:s O', $timestamp); return $dt->toIso8601String(); } catch (\Exception $e) { return $timestamp; } } protected function formatTimestampForDisplay(string $timestamp): string { try { // Parse the Proxmox timestamp format: 04/Dec/2025:06:49:07 -1000 $dt = Carbon::createFromFormat('d/M/Y:H:i:s O', $timestamp); // Convert to user's timezone (using app timezone) $dt->setTimezone(config('app.timezone', 'UTC')); // Format as: Dec 04, 2025 16:49:07 (or use a more readable format) return $dt->format('M d, Y H:i:s'); } catch (\Exception $e) { return $timestamp; } } protected function getLevelLabel(int $level): string { return match($level) { 0 => 'Emergency', 1 => 'Alert', 2 => 'Critical', 3 => 'Error', 4 => 'Warning', 5 => 'Notice', 6 => 'Info', 7 => 'Debug', default => 'Unknown', }; } protected function getLevelBadgeClass(string $action): string { return match(strtoupper($action)) { 'ACCEPT' => 'bg-success/20 text-success', 'DROP' => 'bg-error/20 text-error', 'REJECT' => 'bg-warning/20 text-warning', default => 'bg-base/20 text-base/60', }; } // ==================== IP LOCATION ==================== public function fetchIPLocation(string $ip): array { try { if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return [ 'status' => 'fail', 'message' => 'Invalid or private IP address' ]; } $response = Http::timeout(5)->get("http://ip-api.com/json/{$ip}"); if ($response->successful()) { $data = $response->json(); if (isset($data['status']) && $data['status'] === 'success') { return $data; } return [ 'status' => 'fail', 'message' => $data['message'] ?? 'Unable to fetch location data' ]; } return [ 'status' => 'fail', 'message' => 'Failed to fetch location data (HTTP ' . $response->status() . ')' ]; } catch (\Exception $e) { return [ 'status' => 'fail', 'message' => 'Error: ' . $e->getMessage() ]; } } // ==================== HELPER METHODS ==================== public function checkIsPublicIP(string $ip): bool { if (empty($ip) || $ip === '—' || $ip === '-') { return false; } // Use PHP's filter_var with appropriate flags return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false; } // ==================== NETWORK ANALYSIS INTEGRATION ==================== public function getNetworkAnalysisForLog( string $sourceIP, string $destIP, string $sourcePort, string $destPort, string $protocol ): array { try { // Check if Core Router is configured using the same logic as Statistics component if (!$this->coreRouterEnabled()) { return [ 'connections' => [], 'error' => 'Core Router is not configured' ]; } // Get network analysis from Statistics component logic $client = \Illuminate\Support\Facades\App::make(CoreRouterClient::class); // Fetch all firewall states $allStates = $client->listStates(); // Log the search parameters for debugging Log::info('Searching for network analysis match', [ 'sourceIP' => $sourceIP, 'destIP' => $destIP, 'sourcePort' => $sourcePort, 'destPort' => $destPort, 'protocol' => $protocol, 'total_states' => count($allStates) ]); // Filter states matching the log entry $matchingConnections = []; $debugSamples = []; foreach ($allStates as $state) { $rawSource = (string)($state['source'] ?? ''); $rawDestination = (string)($state['destination'] ?? ''); // Extract IPs from the state $stateSourceIP = $this->extractIpFromField($rawSource); $stateDestIP = $this->extractIpFromField($rawDestination); // Store first 5 for debugging if (count($debugSamples) < 5) { $debugSamples[] = [ 'raw_source' => $rawSource, 'raw_destination' => $rawDestination, 'extracted_source' => $stateSourceIP, 'extracted_dest' => $stateDestIP, ]; } // Extract ports $stateSourcePort = null; $stateDestPort = null; if (preg_match('/\[(\d+)\]$/', $rawSource, $matches)) { $stateSourcePort = (int)$matches[1]; } elseif (preg_match('/:(\d+)$/', $rawSource, $matches) && filter_var(explode(':', $rawSource)[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $stateSourcePort = (int)$matches[1]; } if (preg_match('/\[(\d+)\]$/', $rawDestination, $matches)) { $stateDestPort = (int)$matches[1]; } elseif (preg_match('/:(\d+)$/', $rawDestination, $matches) && filter_var(explode(':', $rawDestination)[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $stateDestPort = (int)$matches[1]; } $stateProtocol = strtoupper($state['protocol'] ?? ''); // Normalize all values for comparison $normalizedSourceIP = strtolower(trim($sourceIP)); $normalizedDestIP = strtolower(trim($destIP)); $normalizedStateSourceIP = strtolower(trim($stateSourceIP)); $normalizedStateDestIP = strtolower(trim($stateDestIP)); $normalizedProtocol = strtoupper(trim($protocol)); // Flexible matching logic - multiple levels of matching $ipMatch = false; $matchType = ''; // Level 1: Exact bidirectional match (both IPs match in either direction) if ($normalizedStateSourceIP === $normalizedSourceIP && $normalizedStateDestIP === $normalizedDestIP) { $ipMatch = true; $matchType = 'exact'; } elseif ($normalizedStateSourceIP === $normalizedDestIP && $normalizedStateDestIP === $normalizedSourceIP) { $ipMatch = true; $matchType = 'exact-reversed'; } // Level 2: Destination IP match (log dest matches either state IP) - catches related traffic elseif ($normalizedStateSourceIP === $normalizedDestIP || $normalizedStateDestIP === $normalizedDestIP) { $ipMatch = true; $matchType = 'dest-partial'; } // Level 3: Source IP match (log source matches either state IP) - catches related traffic elseif ($normalizedStateSourceIP === $normalizedSourceIP || $normalizedStateDestIP === $normalizedSourceIP) { $ipMatch = true; $matchType = 'source-partial'; } // Protocol match - be flexible $protocolMatch = (empty($protocol) || $protocol === '-' || $normalizedProtocol === $stateProtocol); // Port match - check if destination port from log matches any port in state // Also allow match if no port specified or if it's a partial IP match $portMatch = false; if (empty($destPort) || $destPort === '-') { $portMatch = true; // No port specified, match all } else { $destPortNum = (int)$destPort; if ($stateDestPort === $destPortNum || $stateSourcePort === $destPortNum) { $portMatch = true; } // For partial matches, also accept if protocol matches even if port doesn't if (!$portMatch && str_contains($matchType, 'partial') && $protocolMatch) { $portMatch = true; } } // Log first potential match for debugging if ($ipMatch && $protocolMatch && $portMatch && count($matchingConnections) === 0) { Log::info('First match found', [ 'match_type' => $matchType, 'state_source' => $stateSourceIP, 'state_dest' => $stateDestIP, 'state_protocol' => $stateProtocol, 'state_src_port' => $stateSourcePort, 'state_dst_port' => $stateDestPort, 'log_source' => $sourceIP, 'log_dest' => $destIP, 'log_protocol' => $protocol, 'log_dest_port' => $destPort, ]); } if ($ipMatch && $protocolMatch && $portMatch) { $bytes = (int)($state['bytes_total'] ?? $state['bytes'] ?? 0); $packets = (int)($state['packets_total'] ?? $state['packets'] ?? 0); $stateName = strtoupper($state['state'] ?? 'UNKNOWN'); $interface = strtoupper($state['interface'] ?? 'N/A'); $direction = strtolower($state['direction'] ?? '') === 'in' ? 'Inbound' : 'Outbound'; $matchingConnections[] = [ 'protocol' => $stateProtocol, 'source' => $stateSourceIP, 'destination' => $stateDestIP, 'source_port' => $stateSourcePort, 'destination_port' => $stateDestPort, 'raw_source' => $rawSource, 'raw_destination' => $rawDestination, 'bytes' => $bytes, 'bytes_human' => $this->formatBytes($bytes), 'packets' => $packets, 'state' => $stateName, 'interface' => $interface, 'direction' => $direction, ]; } } // Log results for debugging Log::info('Network analysis match results', [ 'matches_found' => count($matchingConnections), 'debug_samples' => $debugSamples ]); // Sort by bytes (highest first) usort($matchingConnections, fn($a, $b) => $b['bytes'] <=> $a['bytes']); return [ 'connections' => $matchingConnections, 'error' => null ]; } catch (\Exception $e) { Log::error('Failed to fetch network analysis for log entry', [ 'error' => $e->getMessage(), 'source' => $sourceIP, 'dest' => $destIP ]); return [ 'connections' => [], 'error' => 'Failed to fetch network data: ' . $e->getMessage() ]; } } protected function coreRouterEnabled(): bool { $baseUrl = config('corerouter.base_url'); $apiKey = config('corerouter.api_key'); $username = config('corerouter.username'); $password = config('corerouter.password'); // Check if base_url is configured and not empty $hasBaseUrl = !empty(trim((string) $baseUrl)); // Check if API key is configured (not empty after trimming) $hasKey = !empty(trim((string) $apiKey)); // Check if basic auth credentials are both configured $hasBasic = !empty(trim((string) $username)) && !empty(trim((string) $password)); // Configuration is valid if base_url is set and at least one auth method is configured return $hasBaseUrl && ($hasKey || $hasBasic); } protected function extractIpFromField(string $field): string { if (empty($field)) { return ''; } $field = trim($field); // Handle IPv4 "IP:port" format FIRST if (preg_match('/^(\d+\.\d+\.\d+\.\d+):(\d+)$/', $field, $matches)) { return $matches[1]; } // Handle "IP[port]" format if (preg_match('/^([^\[\]]+)(?:\[[^\]]+\])?$/', $field, $matches)) { $ip = trim($matches[1]); $ip = rtrim($ip, ':[]'); return $ip; } return $field; } protected function formatBytes(int $bytes): string { if ($bytes === 0) { return '0 B'; } $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; $power = floor(log($bytes, 1024)); $power = min($power, count($units) - 1); return round($bytes / pow(1024, $power), 2) . ' ' . $units[$power]; } public function render() { return view('proxmox::firewall')->layoutData([ 'sidebar' => true, ]); } }