'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' ]; // 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 = 1000; public string $logError = ''; // 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(); $this->loadFirewallLogs(); } // ==================== 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'; $request = Http::withHeaders([ 'Authorization' => $authHeader, 'Accept' => $acceptHeader, ])->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; 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; $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/config', 'put', [$adapter . '.firewall' => $newState ? 1 : 0] ); if ($response->successful()) { $this->adapterFirewallStates[$adapter] = $newState; $this->notify('Adapter firewall ' . ($newState ? 'enabled' : 'disabled'), 'success'); } else { $this->error = 'Failed to toggle adapter firewall'; $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'); $this->ipFilter = ($options['ipfilter'] ?? 1) == 1; $this->macFilter = ($options['macfilter'] ?? 1) == 1; } } 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 and NDP (router advertisements) to 0 for security $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' => 0, // Always disabled (prevents router advertisements) ] ); if ($response->successful()) { $this->firewallEnabled = $newState; $this->notify('Firewall ' . ($newState ? 'enabled' : 'disabled'), 'success'); } else { $this->error = 'Failed to toggle firewall'; $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, 'ipfilter' => $this->ipFilter ? 1 : 0, 'macfilter' => $this->macFilter ? 1 : 0, 'dhcp' => 0, // Always disabled 'ndp' => 0, // Always disabled ] ); 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 ==================== 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, ]; if (!empty(trim($this->ruleMacro))) { $payload['macro'] = trim($this->ruleMacro); } 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 { // Fetch logs in batches to handle large log sets $allRawLogs = []; $start = 0; $batchSize = 500; $maxLogs = $this->logLimit; do { $response = $this->firewallRequest( '/nodes/' . $this->server->node->name . '/qemu/' . $this->server->vm_id . '/firewall/log', 'get', [ 'limit' => $batchSize, 'start' => $start ] ); 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(); $batchRawLogs = []; // 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'])) { $batchRawLogs[] = $item['t']; } elseif (is_string($item)) { $batchRawLogs[] = $item; } } } elseif (is_string($jsonData['data'])) { $lines = preg_split('/\r\n|\r|\n/', $jsonData['data']); $batchRawLogs = array_filter($lines, fn($line) => !empty(trim($line))); } } elseif (is_array($jsonData)) { foreach ($jsonData as $item) { if (is_array($item) && isset($item['t'])) { $batchRawLogs[] = $item['t']; } elseif (is_string($item)) { $batchRawLogs[] = $item; } } } } catch (\Exception $e) { Log::warning('Failed to parse JSON response', ['error' => $e->getMessage()]); } } // Fall back to text/plain format if (empty($batchRawLogs) && !empty($body)) { $lines = preg_split('/\r\n|\r|\n/', $body); $batchRawLogs = array_filter($lines, fn($line) => !empty(trim($line))); } $batchRawLogs = array_values($batchRawLogs); $allRawLogs = array_merge($allRawLogs, $batchRawLogs); $fetchedCount = count($batchRawLogs); $start += $batchSize; // Break if we got fewer logs than requested or reached limit if ($fetchedCount < $batchSize || count($allRawLogs) >= $maxLogs) { break; } } while (true); // Limit to maxLogs if (count($allRawLogs) > $maxLogs) { $allRawLogs = array_slice($allRawLogs, 0, $maxLogs); } 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); Log::info('Firewall log parsing summary', [ 'total_raw_logs' => count($allRawLogs), 'successfully_parsed' => count($this->firewallLogs), ]); } catch (\Exception $e) { $this->logError = 'Failed to load logs: ' . $e->getMessage(); Log::error('Firewall logs error: ' . $e->getMessage()); $this->firewallLogs = []; } } 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; } } 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'] ?? ''), '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 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; } public function render() { return view('proxmox::firewall'); } }