Add caching for firewall states to optimize network analysis performance
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user