diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index 88d0f1283..b8a725ffc 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -8,6 +8,7 @@ use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Paymenter\Extensions\Servers\Proxmox\Models\Server; use Paymenter\Extensions\Servers\Proxmox\Proxmox; +use Paymenter\Extensions\Servers\Proxmox\Services\CoreRouterClient; class Firewall extends Component { @@ -1566,6 +1567,165 @@ class Firewall extends Component 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 + $coreRouterEnabled = \Illuminate\Support\Facades\Config::get('corerouter.enabled', false); + if (!$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(); + + // Filter states matching the log entry + $matchingConnections = []; + + 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); + + // 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'] ?? ''); + + // Check if this state matches the log entry + // Match can be source->dest or dest->source (bidirectional) + $sourceMatch = ( + strtolower(trim($stateSourceIP)) === strtolower(trim($sourceIP)) && + strtolower(trim($stateDestIP)) === strtolower(trim($destIP)) + ); + + $reverseMatch = ( + strtolower(trim($stateSourceIP)) === strtolower(trim($destIP)) && + strtolower(trim($stateDestIP)) === strtolower(trim($sourceIP)) + ); + + // Check protocol match + $protocolMatch = empty($protocol) || + strtoupper(trim($protocol)) === $stateProtocol; + + // Check port match (if specified in log) + $portMatch = true; + if (!empty($destPort) && $destPort !== '-') { + $portMatch = ($stateDestPort == $destPort) || ($stateSourcePort == $destPort); + } + + if (($sourceMatch || $reverseMatch) && $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, + ]; + } + } + + // 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 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([ diff --git a/extensions/Servers/Proxmox/views/firewall.blade.php b/extensions/Servers/Proxmox/views/firewall.blade.php index 5f3ac6056..046af3d72 100644 --- a/extensions/Servers/Proxmox/views/firewall.blade.php +++ b/extensions/Servers/Proxmox/views/firewall.blade.php @@ -844,6 +844,7 @@