update
This commit is contained in:
171
ts/proxies/smart-proxy/security-manager.ts
Normal file
171
ts/proxies/smart-proxy/security-manager.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
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<string, Set<string>> = new Map();
|
||||
private connectionRateByIP: Map<string, number[]> = 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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user