import * as plugins from '../../plugins.js'; import type { SmartProxy } from './smart-proxy.js'; import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js'; import { isIPAuthorized, normalizeIP } from '../../core/utils/security-utils.js'; /** * Handles security aspects like IP tracking, rate limiting, and authorization * for SmartProxy. This is a lightweight wrapper that uses shared utilities. */ export class SecurityManager { private connectionsByIP: Map> = new Map(); private connectionRateByIP: Map = new Map(); private cleanupInterval: NodeJS.Timeout | null = null; constructor(private smartProxy: SmartProxy) { // Start periodic cleanup every 60 seconds this.startPeriodicCleanup(); } /** * Get connections count by IP (checks normalized variants) */ public getConnectionCountByIP(ip: string): number { // Check all normalized variants of the IP const variants = normalizeIP(ip); for (const variant of variants) { const connections = this.connectionsByIP.get(variant); if (connections) { return connections.size; } } return 0; } /** * Check and update connection rate for an IP * @returns true if within rate limit, false if exceeding limit */ public checkConnectionRate(ip: string): boolean { const now = Date.now(); const minute = 60 * 1000; // Find existing rate tracking (check normalized variants) const variants = normalizeIP(ip); let existingKey: string | null = null; for (const variant of variants) { if (this.connectionRateByIP.has(variant)) { existingKey = variant; break; } } const key = existingKey || ip; if (!this.connectionRateByIP.has(key)) { this.connectionRateByIP.set(key, [now]); return true; } // Get timestamps and filter out entries older than 1 minute const timestamps = this.connectionRateByIP.get(key)!.filter((time) => now - time < minute); timestamps.push(now); this.connectionRateByIP.set(key, timestamps); // Check if rate exceeds limit return timestamps.length <= this.smartProxy.settings.connectionRateLimitPerMinute!; } /** * Track connection by IP */ public trackConnectionByIP(ip: string, connectionId: string): void { // Check if any variant already exists const variants = normalizeIP(ip); let existingKey: string | null = null; for (const variant of variants) { if (this.connectionsByIP.has(variant)) { existingKey = variant; break; } } const key = existingKey || ip; if (!this.connectionsByIP.has(key)) { this.connectionsByIP.set(key, new Set()); } this.connectionsByIP.get(key)!.add(connectionId); } /** * Remove connection tracking for an IP */ public removeConnectionByIP(ip: string, connectionId: string): void { // Check all variants to find where the connection is tracked const variants = normalizeIP(ip); for (const variant of variants) { if (this.connectionsByIP.has(variant)) { const connections = this.connectionsByIP.get(variant)!; connections.delete(connectionId); if (connections.size === 0) { this.connectionsByIP.delete(variant); } break; } } } /** * Check if an IP is authorized using security rules * * This method is used to determine if an IP is allowed to connect, based on security * rules configured in the route configuration. The allowed and blocked IPs are * typically derived from route.security.ipAllowList and ipBlockList. * * @param ip - The IP address to check * @param allowedIPs - Array of allowed IP patterns from security.ipAllowList * @param blockedIPs - Array of blocked IP patterns from security.ipBlockList * @returns true if IP is authorized, false if blocked */ public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean { return isIPAuthorized(ip, allowedIPs, blockedIPs); } /** * Check if IP should be allowed considering connection rate and max connections * @returns Object with result and reason */ public validateIP(ip: string): { allowed: boolean; reason?: string } { // Check connection count limit if ( this.smartProxy.settings.maxConnectionsPerIP && this.getConnectionCountByIP(ip) >= this.smartProxy.settings.maxConnectionsPerIP ) { return { allowed: false, reason: `Maximum connections per IP (${this.smartProxy.settings.maxConnectionsPerIP}) exceeded` }; } // Check connection rate limit if ( this.smartProxy.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(ip) ) { return { allowed: false, reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded` }; } return { allowed: true }; } /** * Atomically validate an IP and track the connection if allowed. * This prevents race conditions where concurrent connections could bypass per-IP limits. * * @param ip - The IP address to validate * @param connectionId - The connection ID to track if validation passes * @returns Object with validation result and reason */ public validateAndTrackIP(ip: string, connectionId: string): { allowed: boolean; reason?: string } { // Check connection count limit BEFORE tracking if ( this.smartProxy.settings.maxConnectionsPerIP && this.getConnectionCountByIP(ip) >= this.smartProxy.settings.maxConnectionsPerIP ) { return { allowed: false, reason: `Maximum connections per IP (${this.smartProxy.settings.maxConnectionsPerIP}) exceeded` }; } // Check connection rate limit if ( this.smartProxy.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(ip) ) { return { allowed: false, reason: `Connection rate limit (${this.smartProxy.settings.connectionRateLimitPerMinute}/min) exceeded` }; } // Validation passed - immediately track to prevent race conditions this.trackConnectionByIP(ip, connectionId); return { allowed: true }; } /** * Clears all IP tracking data (for shutdown) */ public clearIPTracking(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.connectionsByIP.clear(); this.connectionRateByIP.clear(); } /** * Start periodic cleanup of expired data */ private startPeriodicCleanup(): void { this.cleanupInterval = setInterval(() => { this.performCleanup(); }, 60000); // Run every minute // Unref the timer so it doesn't keep the process alive if (this.cleanupInterval.unref) { this.cleanupInterval.unref(); } } /** * Perform cleanup of expired rate limits and empty IP entries */ private performCleanup(): void { const now = Date.now(); const minute = 60 * 1000; let cleanedRateLimits = 0; let cleanedIPs = 0; // Clean up expired rate limit timestamps for (const [ip, timestamps] of this.connectionRateByIP.entries()) { const validTimestamps = timestamps.filter(time => now - time < minute); if (validTimestamps.length === 0) { // No valid timestamps, remove the IP entry this.connectionRateByIP.delete(ip); cleanedRateLimits++; } else if (validTimestamps.length < timestamps.length) { // Some timestamps expired, update with valid ones this.connectionRateByIP.set(ip, validTimestamps); } } // Clean up IPs with no active connections for (const [ip, connections] of this.connectionsByIP.entries()) { if (connections.size === 0) { this.connectionsByIP.delete(ip); cleanedIPs++; } } // Log cleanup stats if anything was cleaned if (cleanedRateLimits > 0 || cleanedIPs > 0) { if (this.smartProxy.settings.enableDetailedLogging) { connectionLogDeduplicator.log( 'ip-cleanup', 'debug', 'IP tracking cleanup completed', { cleanedRateLimits, cleanedIPs, remainingIPs: this.connectionsByIP.size, remainingRateLimits: this.connectionRateByIP.size, component: 'security-manager' }, 'periodic-cleanup' ); } } } }