feat(proxies): introduce nftables command executor and utilities, default certificate provider, expanded route/socket helper modules, and security improvements

This commit is contained in:
2026-01-30 04:06:32 +00:00
parent f25be4c55a
commit 9697ab3078
27 changed files with 2453 additions and 2048 deletions

View File

@@ -1,10 +1,11 @@
import * as plugins from '../../plugins.js';
import type { SmartProxy } from './smart-proxy.js';
import { logger } from '../../core/utils/logger.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();
@@ -15,14 +16,22 @@ export class SecurityManager {
// Start periodic cleanup every 60 seconds
this.startPeriodicCleanup();
}
/**
* Get connections count by IP
* Get connections count by IP (checks normalized variants)
*/
public getConnectionCountByIP(ip: string): number {
return this.connectionsByIP.get(ip)?.size || 0;
// 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
@@ -31,43 +40,73 @@ export class SecurityManager {
const now = Date.now();
const minute = 60 * 1000;
if (!this.connectionRateByIP.has(ip)) {
this.connectionRateByIP.set(ip, [now]);
// 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(ip)!.filter((time) => now - time < minute);
const timestamps = this.connectionRateByIP.get(key)!.filter((time) => now - time < minute);
timestamps.push(now);
this.connectionRateByIP.set(ip, timestamps);
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 {
if (!this.connectionsByIP.has(ip)) {
this.connectionsByIP.set(ip, new Set());
// 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;
}
}
this.connectionsByIP.get(ip)!.add(connectionId);
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 {
if (this.connectionsByIP.has(ip)) {
const connections = this.connectionsByIP.get(ip)!;
connections.delete(connectionId);
if (connections.size === 0) {
this.connectionsByIP.delete(ip);
// 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
*
@@ -81,71 +120,7 @@ export class SecurityManager {
* @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 route.security configuration.
*
* @param ip - The IP address to check
* @param patterns - Array of glob patterns from security.ipAllowList or ipBlockList
* @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;
// Expand shorthand patterns and normalize IPs for consistent comparison
const expandShorthand = (pattern: string): string => {
// Expand shorthand IP patterns like '192.168.*' to '192.168.*.*'
if (pattern.includes('*') && !pattern.includes(':')) {
const parts = pattern.split('.');
while (parts.length < 4) {
parts.push('*');
}
return parts.join('.');
}
return pattern;
};
const expandedPatterns = patterns.map(expandShorthand).flatMap(normalizeIP);
// Check for any match between normalized IP variants and patterns
return normalizedIPVariants.some((ipVariant) =>
expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
);
return isIPAuthorized(ip, allowedIPs, blockedIPs);
}
/**