269 lines
8.4 KiB
TypeScript
269 lines
8.4 KiB
TypeScript
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<string, Set<string>> = new Map();
|
|
private connectionRateByIP: Map<string, number[]> = 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'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} |