import * as plugins from '../../plugins.js'; import type { SmartProxyOptions } from './models/interfaces.js'; /** * Handles security aspects like IP tracking, rate limiting, and authorization */ export class SecurityManager { private connectionsByIP: Map> = new Map(); private connectionRateByIP: Map = new Map(); constructor(private settings: SmartProxyOptions) {} /** * Get connections count by IP */ public getConnectionCountByIP(ip: string): number { return this.connectionsByIP.get(ip)?.size || 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; if (!this.connectionRateByIP.has(ip)) { this.connectionRateByIP.set(ip, [now]); return true; } // Get timestamps and filter out entries older than 1 minute const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute); timestamps.push(now); this.connectionRateByIP.set(ip, timestamps); // Check if rate exceeds limit return timestamps.length <= this.settings.connectionRateLimitPerMinute!; } /** * Track connection by IP */ public trackConnectionByIP(ip: string, connectionId: string): void { if (!this.connectionsByIP.has(ip)) { this.connectionsByIP.set(ip, new Set()); } this.connectionsByIP.get(ip)!.add(connectionId); } /** * Remove connection tracking for an IP */ public removeConnectionByIP(ip: string, connectionId: string): void { if (this.connectionsByIP.has(ip)) { const connections = this.connectionsByIP.get(ip)!; connections.delete(connectionId); if (connections.size === 0) { this.connectionsByIP.delete(ip); } } } /** * Check if an IP is authorized using forwarding security rules * * This method is used to determine if an IP is allowed to connect, based on security * rules configured in the forwarding configuration. The allowed and blocked IPs are * typically derived from domain.forwarding.security.allowedIps and blockedIps through * DomainConfigManager.getEffectiveIPRules(). * * @param ip - The IP address to check * @param allowedIPs - Array of allowed IP patterns from forwarding.security.allowedIps * @param blockedIPs - Array of blocked IP patterns from forwarding.security.blockedIps * @returns true if IP is authorized, false if blocked */ public isIPAuthorized(ip: string, allowedIPs: string[], blockedIPs: string[] = []): boolean { // Skip IP validation if allowedIPs is empty if (!ip || (allowedIPs.length === 0 && blockedIPs.length === 0)) { return true; } // First check if IP is blocked - blocked IPs take precedence if (blockedIPs.length > 0 && this.isGlobIPMatch(ip, blockedIPs)) { return false; } // Then check if IP is allowed return this.isGlobIPMatch(ip, allowedIPs); } /** * Check if the IP matches any of the glob patterns from security configuration * * This method checks IP addresses against glob patterns and handles IPv4/IPv6 normalization. * It's used to implement IP filtering based on the forwarding.security configuration. * * @param ip - The IP address to check * @param patterns - Array of glob patterns from forwarding.security.allowedIps or blockedIps * @returns true if IP matches any pattern, false otherwise */ private isGlobIPMatch(ip: string, patterns: string[]): boolean { if (!ip || !patterns || patterns.length === 0) return false; // Handle IPv4/IPv6 normalization for proper matching const normalizeIP = (ip: string): string[] => { if (!ip) return []; // Handle IPv4-mapped IPv6 addresses (::ffff:127.0.0.1) if (ip.startsWith('::ffff:')) { const ipv4 = ip.slice(7); return [ip, ipv4]; } // Handle IPv4 addresses by also checking IPv4-mapped form if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) { return [ip, `::ffff:${ip}`]; } return [ip]; }; // Normalize the IP being checked const normalizedIPVariants = normalizeIP(ip); if (normalizedIPVariants.length === 0) return false; // Normalize the pattern IPs for consistent comparison const expandedPatterns = patterns.flatMap(normalizeIP); // Check for any match between normalized IP variants and patterns return normalizedIPVariants.some((ipVariant) => expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern)) ); } /** * 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.settings.maxConnectionsPerIP && this.getConnectionCountByIP(ip) >= this.settings.maxConnectionsPerIP ) { return { allowed: false, reason: `Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded` }; } // Check connection rate limit if ( this.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(ip) ) { return { allowed: false, reason: `Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded` }; } return { allowed: true }; } /** * Clears all IP tracking data (for shutdown) */ public clearIPTracking(): void { this.connectionsByIP.clear(); this.connectionRateByIP.clear(); } }