Add caching for firewall states to optimize network analysis performance

This commit is contained in:
2025-12-10 12:02:45 -05:00
parent 835fc9216d
commit 00d4a50e9d

View File

@@ -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) {