From 00d4a50e9d82453b53bda8a2c24957e6cf954807 Mon Sep 17 00:00:00 2001 From: "Joseph.Rawlings" Date: Wed, 10 Dec 2025 12:02:45 -0500 Subject: [PATCH] Add caching for firewall states to optimize network analysis performance --- .../Servers/Proxmox/Livewire/Firewall.php | 283 ++++++++---------- 1 file changed, 131 insertions(+), 152 deletions(-) diff --git a/extensions/Servers/Proxmox/Livewire/Firewall.php b/extensions/Servers/Proxmox/Livewire/Firewall.php index f196027d2..29298a866 100644 --- a/extensions/Servers/Proxmox/Livewire/Firewall.php +++ b/extensions/Servers/Proxmox/Livewire/Firewall.php @@ -1625,6 +1625,11 @@ class Firewall extends Component // ==================== NETWORK ANALYSIS INTEGRATION ==================== + // Cache for firewall states to avoid repeated API calls + protected ?array $cachedStates = null; + protected ?int $cacheTimestamp = null; + protected const STATE_CACHE_TTL = 30; // Cache states for 30 seconds + public function getNetworkAnalysisForLog( string $sourceIP, string $destIP, @@ -1633,7 +1638,7 @@ class Firewall extends Component string $protocol ): array { try { - // Check if Core Router is configured using the same logic as Statistics component + // Check if Core Router is configured if (!$this->coreRouterEnabled()) { return [ 'connections' => [], @@ -1641,160 +1646,97 @@ class Firewall extends Component ]; } - // Get network analysis from Statistics component logic $client = \Illuminate\Support\Facades\App::make(CoreRouterClient::class); - // Fetch all firewall states - $allStates = $client->listStates(); + // Use cached states if available and not expired + $now = time(); + if ($this->cachedStates === null || + $this->cacheTimestamp === null || + ($now - $this->cacheTimestamp) > self::STATE_CACHE_TTL) { + $this->cachedStates = $client->listStates(); + $this->cacheTimestamp = $now; + } - // 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) - ]); + $allStates = $this->cachedStates; + + // Normalize search parameters once + $normalizedSourceIP = strtolower(trim($sourceIP)); + $normalizedDestIP = strtolower(trim($destIP)); + $normalizedProtocol = strtoupper(trim($protocol)); + $destPortNum = ($destPort && $destPort !== '-') ? (int)$destPort : null; // Filter states matching the log entry $matchingConnections = []; - $debugSamples = []; + $maxMatches = 50; // Limit results for performance foreach ($allStates as $state) { + if (count($matchingConnections) >= $maxMatches) { + break; + } + $rawSource = (string)($state['source'] ?? ''); $rawDestination = (string)($state['destination'] ?? ''); + // Quick check - skip if source IP not present in either field + if (strpos(strtolower($rawSource), $normalizedSourceIP) === false && + strpos(strtolower($rawDestination), $normalizedSourceIP) === false) { + continue; + } + // Extract IPs from the state - $stateSourceIP = $this->extractIpFromField($rawSource); - $stateDestIP = $this->extractIpFromField($rawDestination); + $stateSourceIP = $this->extractIpFromFieldFast($rawSource); + $stateDestIP = $this->extractIpFromFieldFast($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)); - - // Matching logic - Source IP from log MUST always match - // The source IP can appear as either source or destination in the state (for return traffic) - $ipMatch = false; - $matchType = ''; + $normalizedStateSourceIP = strtolower($stateSourceIP); + $normalizedStateDestIP = strtolower($stateDestIP); // Check if the log's source IP appears in the state $sourceInState = ($normalizedStateSourceIP === $normalizedSourceIP || $normalizedStateDestIP === $normalizedSourceIP); if (!$sourceInState) { - // Source IP not found in this state - skip it continue; } - // Source IP matches, now check destination - // Level 1: Exact 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: Source IP matches, destination is flexible (shows all traffic from this source) - elseif ($sourceInState) { - $ipMatch = true; - $matchType = 'source-match'; + // Protocol match + $stateProtocol = strtoupper($state['protocol'] ?? ''); + if (!empty($protocol) && $protocol !== '-' && $normalizedProtocol !== $stateProtocol) { + continue; } - // Protocol match - be flexible - $protocolMatch = (empty($protocol) || $protocol === '-' || $normalizedProtocol === $stateProtocol); - - // Port match - destination port from log MUST match a port in state - $portMatch = false; - if (empty($destPort) || $destPort === '-') { - $portMatch = true; // No port specified in log, match all - } else { - $destPortNum = (int)$destPort; - // Destination port must match either state's dest or source port (for return traffic) - if ($stateDestPort === $destPortNum || $stateSourcePort === $destPortNum) { - $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'; + // Port match if specified + if ($destPortNum !== null) { + $stateSourcePort = $this->extractPortFromField($rawSource); + $stateDestPort = $this->extractPortFromField($rawDestination); - $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, - ]; + if ($stateDestPort !== $destPortNum && $stateSourcePort !== $destPortNum) { + continue; + } + } else { + $stateSourcePort = $this->extractPortFromField($rawSource); + $stateDestPort = $this->extractPortFromField($rawDestination); } + + // Match found - build connection data + $bytes = (int)($state['bytes_total'] ?? $state['bytes'] ?? 0); + $packets = (int)($state['packets_total'] ?? $state['packets'] ?? 0); + + $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' => strtoupper($state['state'] ?? 'UNKNOWN'), + 'interface' => strtoupper($state['interface'] ?? 'N/A'), + 'direction' => strtolower($state['direction'] ?? '') === 'in' ? 'Inbound' : 'Outbound', + ]; } - // 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']); @@ -1807,7 +1749,6 @@ class Firewall extends Component Log::error('Failed to fetch network analysis for log entry', [ 'error' => $e->getMessage(), 'source' => $sourceIP, - 'dest' => $destIP ]); return [ @@ -1817,6 +1758,67 @@ class Firewall extends Component } } + /** + * Fast IP extraction without regex for common formats + */ + protected function extractIpFromFieldFast(string $field): string + { + if (empty($field)) { + return ''; + } + + $field = trim($field); + + // Handle IPv4 "IP:port" format - most common + $colonPos = strrpos($field, ':'); + if ($colonPos !== false) { + $potentialIp = substr($field, 0, $colonPos); + // Check if it's a valid IPv4 + if (filter_var($potentialIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return $potentialIp; + } + } + + // Handle "IP[port]" format + $bracketPos = strpos($field, '['); + if ($bracketPos !== false) { + return substr($field, 0, $bracketPos); + } + + // Return as-is if it's a valid IP + if (filter_var($field, FILTER_VALIDATE_IP)) { + return $field; + } + + return $field; + } + + /** + * Fast port extraction + */ + protected function extractPortFromField(string $field): ?int + { + if (empty($field)) { + return null; + } + + // Handle "[port]" format + if (preg_match('/\[(\d+)\]$/', $field, $matches)) { + return (int)$matches[1]; + } + + // Handle "IP:port" format for IPv4 + $colonPos = strrpos($field, ':'); + if ($colonPos !== false) { + $potentialIp = substr($field, 0, $colonPos); + if (filter_var($potentialIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return (int)substr($field, $colonPos + 1); + } + } + + return null; + } + protected function coreRouterEnabled(): bool { $baseUrl = config('corerouter.base_url'); @@ -1837,29 +1839,6 @@ class Firewall extends Component 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) {