better logging
This commit is contained in:
@ -17,6 +17,8 @@ import { WebSocketHandler } from './websocket-handler.js';
|
||||
import { HttpRouter } from '../../routing/router/index.js';
|
||||
import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
||||
import { FunctionCache } from './function-cache.js';
|
||||
import { SecurityManager } from './security-manager.js';
|
||||
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
||||
|
||||
/**
|
||||
* HttpProxy provides a reverse proxy with TLS termination, WebSocket support,
|
||||
@ -43,6 +45,7 @@ export class HttpProxy implements IMetricsTracker {
|
||||
private router = new HttpRouter(); // Unified HTTP router
|
||||
private routeManager: RouteManager;
|
||||
private functionCache: FunctionCache;
|
||||
private securityManager: SecurityManager;
|
||||
|
||||
// State tracking
|
||||
public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
|
||||
@ -113,6 +116,14 @@ export class HttpProxy implements IMetricsTracker {
|
||||
maxCacheSize: this.options.functionCacheSize || 1000,
|
||||
defaultTtl: this.options.functionCacheTtl || 5000
|
||||
});
|
||||
|
||||
// Initialize security manager
|
||||
this.securityManager = new SecurityManager(
|
||||
this.logger,
|
||||
[],
|
||||
this.options.maxConnectionsPerIP || 100,
|
||||
this.options.connectionRateLimitPerMinute || 300
|
||||
);
|
||||
|
||||
// Initialize other components
|
||||
this.certificateManager = new CertificateManager(this.options);
|
||||
@ -269,14 +280,113 @@ export class HttpProxy implements IMetricsTracker {
|
||||
*/
|
||||
private setupConnectionTracking(): void {
|
||||
this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
|
||||
// Check if max connections reached
|
||||
let remoteIP = connection.remoteAddress || '';
|
||||
const connectionId = Math.random().toString(36).substring(2, 15);
|
||||
const isFromSmartProxy = this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1');
|
||||
|
||||
// For SmartProxy connections, wait for CLIENT_IP header
|
||||
if (isFromSmartProxy) {
|
||||
let headerBuffer = Buffer.alloc(0);
|
||||
let headerParsed = false;
|
||||
|
||||
const parseHeader = (data: Buffer) => {
|
||||
if (headerParsed) return data;
|
||||
|
||||
headerBuffer = Buffer.concat([headerBuffer, data]);
|
||||
const headerStr = headerBuffer.toString();
|
||||
const headerEnd = headerStr.indexOf('\r\n');
|
||||
|
||||
if (headerEnd !== -1) {
|
||||
const header = headerStr.substring(0, headerEnd);
|
||||
if (header.startsWith('CLIENT_IP:')) {
|
||||
remoteIP = header.substring(10); // Extract IP after "CLIENT_IP:"
|
||||
this.logger.debug(`Extracted client IP from SmartProxy: ${remoteIP}`);
|
||||
}
|
||||
headerParsed = true;
|
||||
|
||||
// Store the real IP on the connection
|
||||
(connection as any)._realRemoteIP = remoteIP;
|
||||
|
||||
// Validate the real IP
|
||||
const ipValidation = this.securityManager.validateIP(remoteIP);
|
||||
if (!ipValidation.allowed) {
|
||||
connectionLogDeduplicator.log(
|
||||
'ip-rejected',
|
||||
'warn',
|
||||
`HttpProxy connection rejected (via SmartProxy)`,
|
||||
{ remoteIP, reason: ipValidation.reason, component: 'http-proxy' },
|
||||
remoteIP
|
||||
);
|
||||
connection.destroy();
|
||||
return null;
|
||||
}
|
||||
|
||||
// Track connection by real IP
|
||||
this.securityManager.trackConnectionByIP(remoteIP, connectionId);
|
||||
|
||||
// Return remaining data after header
|
||||
return headerBuffer.slice(headerEnd + 2);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Override the first data handler to parse header
|
||||
const originalEmit = connection.emit;
|
||||
connection.emit = function(event: string, ...args: any[]) {
|
||||
if (event === 'data' && !headerParsed) {
|
||||
const remaining = parseHeader(args[0]);
|
||||
if (remaining && remaining.length > 0) {
|
||||
// Call original emit with remaining data
|
||||
return originalEmit.apply(connection, ['data', remaining]);
|
||||
} else if (headerParsed) {
|
||||
// Header parsed but no remaining data
|
||||
return true;
|
||||
}
|
||||
// Header not complete yet, suppress this data event
|
||||
return true;
|
||||
}
|
||||
return originalEmit.apply(connection, [event, ...args]);
|
||||
} as any;
|
||||
} else {
|
||||
// Direct connection - validate immediately
|
||||
const ipValidation = this.securityManager.validateIP(remoteIP);
|
||||
if (!ipValidation.allowed) {
|
||||
connectionLogDeduplicator.log(
|
||||
'ip-rejected',
|
||||
'warn',
|
||||
`HttpProxy connection rejected`,
|
||||
{ remoteIP, reason: ipValidation.reason, component: 'http-proxy' },
|
||||
remoteIP
|
||||
);
|
||||
connection.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Track connection by IP
|
||||
this.securityManager.trackConnectionByIP(remoteIP, connectionId);
|
||||
}
|
||||
|
||||
// Then check global max connections
|
||||
if (this.socketMap.getArray().length >= this.options.maxConnections) {
|
||||
this.logger.warn(`Max connections (${this.options.maxConnections}) reached, rejecting new connection`);
|
||||
connectionLogDeduplicator.log(
|
||||
'connection-rejected',
|
||||
'warn',
|
||||
'HttpProxy max connections reached',
|
||||
{
|
||||
reason: 'global-limit',
|
||||
currentConnections: this.socketMap.getArray().length,
|
||||
maxConnections: this.options.maxConnections,
|
||||
component: 'http-proxy'
|
||||
},
|
||||
'http-proxy-global-limit'
|
||||
);
|
||||
connection.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add connection to tracking
|
||||
|
||||
// Add connection to tracking with metadata
|
||||
(connection as any)._connectionId = connectionId;
|
||||
(connection as any)._remoteIP = remoteIP;
|
||||
this.socketMap.add(connection);
|
||||
this.connectedClients = this.socketMap.getArray().length;
|
||||
|
||||
@ -284,12 +394,12 @@ export class HttpProxy implements IMetricsTracker {
|
||||
const localPort = connection.localPort || 0;
|
||||
const remotePort = connection.remotePort || 0;
|
||||
|
||||
// If this connection is from a SmartProxy (usually indicated by it coming from localhost)
|
||||
if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
|
||||
// If this connection is from a SmartProxy
|
||||
if (isFromSmartProxy) {
|
||||
this.portProxyConnections++;
|
||||
this.logger.debug(`New connection from SmartProxy (local: ${localPort}, remote: ${remotePort})`);
|
||||
this.logger.debug(`New connection from SmartProxy for client ${remoteIP} (local: ${localPort}, remote: ${remotePort})`);
|
||||
} else {
|
||||
this.logger.debug(`New direct connection (local: ${localPort}, remote: ${remotePort})`);
|
||||
this.logger.debug(`New direct connection from ${remoteIP} (local: ${localPort}, remote: ${remotePort})`);
|
||||
}
|
||||
|
||||
// Setup connection cleanup handlers
|
||||
@ -298,12 +408,19 @@ export class HttpProxy implements IMetricsTracker {
|
||||
this.socketMap.remove(connection);
|
||||
this.connectedClients = this.socketMap.getArray().length;
|
||||
|
||||
// Remove IP tracking
|
||||
const connId = (connection as any)._connectionId;
|
||||
const connIP = (connection as any)._realRemoteIP || (connection as any)._remoteIP;
|
||||
if (connId && connIP) {
|
||||
this.securityManager.removeConnectionByIP(connIP, connId);
|
||||
}
|
||||
|
||||
// If this was a SmartProxy connection, decrement the counter
|
||||
if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
|
||||
this.portProxyConnections--;
|
||||
}
|
||||
|
||||
this.logger.debug(`Connection closed. ${this.connectedClients} connections remaining`);
|
||||
this.logger.debug(`Connection closed from ${connIP || 'unknown'}. ${this.connectedClients} connections remaining`);
|
||||
}
|
||||
};
|
||||
|
||||
@ -480,6 +597,9 @@ export class HttpProxy implements IMetricsTracker {
|
||||
|
||||
// Certificate management cleanup is handled by SmartCertManager
|
||||
|
||||
// Flush any pending deduplicated logs
|
||||
connectionLogDeduplicator.flushAll();
|
||||
|
||||
// Close the HTTPS server
|
||||
return new Promise((resolve) => {
|
||||
this.httpsServer.close(() => {
|
||||
|
@ -45,6 +45,10 @@ export interface IHttpProxyOptions {
|
||||
|
||||
// Direct route configurations
|
||||
routes?: IRouteConfig[];
|
||||
|
||||
// Rate limiting and security
|
||||
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
||||
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,7 +14,14 @@ export class SecurityManager {
|
||||
// Store rate limits per route and key
|
||||
private rateLimits: Map<string, Map<string, { count: number, expiry: number }>> = new Map();
|
||||
|
||||
constructor(private logger: ILogger, private routes: IRouteConfig[] = []) {}
|
||||
// Connection tracking by IP
|
||||
private connectionsByIP: Map<string, Set<string>> = new Map();
|
||||
private connectionRateByIP: Map<string, number[]> = new Map();
|
||||
|
||||
constructor(private logger: ILogger, private routes: IRouteConfig[] = [], private maxConnectionsPerIP: number = 100, private connectionRateLimitPerMinute: number = 300) {
|
||||
// Start periodic cleanup for connection tracking
|
||||
this.startPeriodicIpCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the routes configuration
|
||||
@ -295,4 +302,132 @@ export class SecurityManager {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.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 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.getConnectionCountByIP(ip) >= this.maxConnectionsPerIP) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Maximum connections per IP (${this.maxConnectionsPerIP}) exceeded`
|
||||
};
|
||||
}
|
||||
|
||||
// Check connection rate limit
|
||||
if (!this.checkConnectionRate(ip)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Connection rate limit (${this.connectionRateLimitPerMinute}/min) exceeded`
|
||||
};
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all IP tracking data (for shutdown)
|
||||
*/
|
||||
public clearIPTracking(): void {
|
||||
this.connectionsByIP.clear();
|
||||
this.connectionRateByIP.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start periodic cleanup of IP tracking data
|
||||
*/
|
||||
private startPeriodicIpCleanup(): void {
|
||||
// Clean up IP tracking data every minute
|
||||
setInterval(() => {
|
||||
this.performIpCleanup();
|
||||
}, 60000).unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform cleanup of expired IP data
|
||||
*/
|
||||
private performIpCleanup(): 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) {
|
||||
this.connectionRateByIP.delete(ip);
|
||||
cleanedRateLimits++;
|
||||
} else if (validTimestamps.length < timestamps.length) {
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedRateLimits > 0 || cleanedIPs > 0) {
|
||||
this.logger.debug(`IP cleanup: removed ${cleanedIPs} IPs and ${cleanedRateLimits} rate limits`);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IConnectionRecord } from './models/interfaces.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
||||
import { LifecycleComponent } from '../../core/utils/lifecycle-component.js';
|
||||
import { cleanupSocket } from '../../core/utils/socket-utils.js';
|
||||
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
|
||||
@ -26,6 +27,10 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
// Cleanup queue for batched processing
|
||||
private cleanupQueue: Set<string> = new Set();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
private isProcessingCleanup: boolean = false;
|
||||
|
||||
// Route-level connection tracking
|
||||
private connectionsByRoute: Map<string, Set<string>> = new Map();
|
||||
|
||||
constructor(
|
||||
private smartProxy: SmartProxy
|
||||
@ -56,11 +61,19 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
public createConnection(socket: plugins.net.Socket | WrappedSocket): IConnectionRecord | null {
|
||||
// Enforce connection limit
|
||||
if (this.connectionRecords.size >= this.maxConnections) {
|
||||
logger.log('warn', `Connection limit reached (${this.maxConnections}). Rejecting new connection.`, {
|
||||
currentConnections: this.connectionRecords.size,
|
||||
maxConnections: this.maxConnections,
|
||||
component: 'connection-manager'
|
||||
});
|
||||
// Use deduplicated logging for connection limit
|
||||
connectionLogDeduplicator.log(
|
||||
'connection-rejected',
|
||||
'warn',
|
||||
'Global connection limit reached',
|
||||
{
|
||||
reason: 'global-limit',
|
||||
currentConnections: this.connectionRecords.size,
|
||||
maxConnections: this.maxConnections,
|
||||
component: 'connection-manager'
|
||||
},
|
||||
'global-limit'
|
||||
);
|
||||
socket.destroy();
|
||||
return null;
|
||||
}
|
||||
@ -165,18 +178,53 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
return this.connectionRecords.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track connection by route
|
||||
*/
|
||||
public trackConnectionByRoute(routeId: string, connectionId: string): void {
|
||||
if (!this.connectionsByRoute.has(routeId)) {
|
||||
this.connectionsByRoute.set(routeId, new Set());
|
||||
}
|
||||
this.connectionsByRoute.get(routeId)!.add(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove connection tracking for a route
|
||||
*/
|
||||
public removeConnectionByRoute(routeId: string, connectionId: string): void {
|
||||
if (this.connectionsByRoute.has(routeId)) {
|
||||
const connections = this.connectionsByRoute.get(routeId)!;
|
||||
connections.delete(connectionId);
|
||||
if (connections.size === 0) {
|
||||
this.connectionsByRoute.delete(routeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection count by route
|
||||
*/
|
||||
public getConnectionCountByRoute(routeId: string): number {
|
||||
return this.connectionsByRoute.get(routeId)?.size || 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates cleanup once for a connection
|
||||
*/
|
||||
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('info', `Connection cleanup initiated`, {
|
||||
// Use deduplicated logging for cleanup events
|
||||
connectionLogDeduplicator.log(
|
||||
'connection-cleanup',
|
||||
'info',
|
||||
`Connection cleanup: ${reason}`,
|
||||
{
|
||||
connectionId: record.id,
|
||||
remoteIP: record.remoteIP,
|
||||
reason,
|
||||
component: 'connection-manager'
|
||||
});
|
||||
}
|
||||
},
|
||||
reason
|
||||
);
|
||||
|
||||
if (record.incomingTerminationReason == null) {
|
||||
record.incomingTerminationReason = reason;
|
||||
@ -200,10 +248,10 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
|
||||
this.cleanupQueue.add(connectionId);
|
||||
|
||||
// Process immediately if queue is getting large
|
||||
if (this.cleanupQueue.size >= this.cleanupBatchSize) {
|
||||
// Process immediately if queue is getting large and not already processing
|
||||
if (this.cleanupQueue.size >= this.cleanupBatchSize && !this.isProcessingCleanup) {
|
||||
this.processCleanupQueue();
|
||||
} else if (!this.cleanupTimer) {
|
||||
} else if (!this.cleanupTimer && !this.isProcessingCleanup) {
|
||||
// Otherwise, schedule batch processing
|
||||
this.cleanupTimer = this.setTimeout(() => {
|
||||
this.processCleanupQueue();
|
||||
@ -215,27 +263,40 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
* Process the cleanup queue in batches
|
||||
*/
|
||||
private processCleanupQueue(): void {
|
||||
// Prevent concurrent processing
|
||||
if (this.isProcessingCleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessingCleanup = true;
|
||||
|
||||
if (this.cleanupTimer) {
|
||||
this.clearTimeout(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
|
||||
const toCleanup = Array.from(this.cleanupQueue).slice(0, this.cleanupBatchSize);
|
||||
|
||||
// Remove only the items we're processing, not the entire queue!
|
||||
for (const connectionId of toCleanup) {
|
||||
this.cleanupQueue.delete(connectionId);
|
||||
const record = this.connectionRecords.get(connectionId);
|
||||
if (record) {
|
||||
this.cleanupConnection(record, record.incomingTerminationReason || 'normal');
|
||||
try {
|
||||
// Take a snapshot of items to process
|
||||
const toCleanup = Array.from(this.cleanupQueue).slice(0, this.cleanupBatchSize);
|
||||
|
||||
// Remove only the items we're processing from the queue
|
||||
for (const connectionId of toCleanup) {
|
||||
this.cleanupQueue.delete(connectionId);
|
||||
const record = this.connectionRecords.get(connectionId);
|
||||
if (record) {
|
||||
this.cleanupConnection(record, record.incomingTerminationReason || 'normal');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Always reset the processing flag
|
||||
this.isProcessingCleanup = false;
|
||||
|
||||
// Check if more items were added while we were processing
|
||||
if (this.cleanupQueue.size > 0) {
|
||||
this.cleanupTimer = this.setTimeout(() => {
|
||||
this.processCleanupQueue();
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// If there are more in queue, schedule next batch
|
||||
if (this.cleanupQueue.size > 0) {
|
||||
this.cleanupTimer = this.setTimeout(() => {
|
||||
this.processCleanupQueue();
|
||||
}, 10);
|
||||
}
|
||||
}
|
||||
|
||||
@ -252,6 +313,11 @@ export class ConnectionManager extends LifecycleComponent {
|
||||
// Track connection termination
|
||||
this.smartProxy.securityManager.removeConnectionByIP(record.remoteIP, record.id);
|
||||
|
||||
// Remove from route tracking
|
||||
if (record.routeId) {
|
||||
this.removeConnectionByRoute(record.routeId, record.id);
|
||||
}
|
||||
|
||||
// Remove from metrics tracking
|
||||
if (this.smartProxy.metricsCollector) {
|
||||
this.smartProxy.metricsCollector.removeConnection(record.id);
|
||||
|
@ -121,6 +121,11 @@ export class HttpProxyBridge {
|
||||
proxySocket.on('error', reject);
|
||||
});
|
||||
|
||||
// Send client IP information header first (custom protocol)
|
||||
// Format: "CLIENT_IP:<ip>\r\n"
|
||||
const clientIPHeader = Buffer.from(`CLIENT_IP:${record.remoteIP}\r\n`);
|
||||
proxySocket.write(clientIPHeader);
|
||||
|
||||
// Send initial chunk if present
|
||||
if (initialChunk) {
|
||||
// Count the initial chunk bytes
|
||||
|
@ -165,6 +165,7 @@ export interface IConnectionRecord {
|
||||
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
|
||||
hasReceivedInitialData: boolean; // Whether initial data has been received
|
||||
routeConfig?: IRouteConfig; // Associated route config for this connection
|
||||
routeId?: string; // ID of the route this connection is associated with
|
||||
|
||||
// Target information (for dynamic port/host mapping)
|
||||
targetHost?: string; // Resolved target host
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
||||
// Route checking functions have been removed
|
||||
import type { IRouteConfig, IRouteAction } from './models/route-types.js';
|
||||
import type { IRouteContext } from '../../core/models/route-context.js';
|
||||
@ -563,12 +564,20 @@ export class RouteConnectionHandler {
|
||||
);
|
||||
|
||||
if (!isIPAllowed) {
|
||||
logger.log('warn', `IP ${remoteIP} blocked by route security for route ${route.name || 'unnamed'} (connection: ${connectionId})`, {
|
||||
connectionId,
|
||||
remoteIP,
|
||||
routeName: route.name || 'unnamed',
|
||||
component: 'route-handler'
|
||||
});
|
||||
// Deduplicated logging for route IP blocks
|
||||
connectionLogDeduplicator.log(
|
||||
'ip-rejected',
|
||||
'warn',
|
||||
`IP blocked by route security`,
|
||||
{
|
||||
connectionId,
|
||||
remoteIP,
|
||||
routeName: route.name || 'unnamed',
|
||||
reason: 'route-ip-blocked',
|
||||
component: 'route-handler'
|
||||
},
|
||||
remoteIP
|
||||
);
|
||||
socket.end();
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'route_ip_blocked');
|
||||
return;
|
||||
@ -577,14 +586,28 @@ export class RouteConnectionHandler {
|
||||
|
||||
// Check max connections per route
|
||||
if (route.security.maxConnections !== undefined) {
|
||||
// TODO: Implement per-route connection tracking
|
||||
// For now, log that this feature is not yet implemented
|
||||
if (this.smartProxy.settings.enableDetailedLogging) {
|
||||
logger.log('warn', `Route ${route.name} has maxConnections=${route.security.maxConnections} configured but per-route connection limits are not yet implemented`, {
|
||||
connectionId,
|
||||
routeName: route.name,
|
||||
component: 'route-handler'
|
||||
});
|
||||
const routeId = route.id || route.name || 'unnamed';
|
||||
const currentConnections = this.smartProxy.connectionManager.getConnectionCountByRoute(routeId);
|
||||
|
||||
if (currentConnections >= route.security.maxConnections) {
|
||||
// Deduplicated logging for route connection limits
|
||||
connectionLogDeduplicator.log(
|
||||
'connection-rejected',
|
||||
'warn',
|
||||
`Route connection limit reached`,
|
||||
{
|
||||
connectionId,
|
||||
routeName: route.name,
|
||||
currentConnections,
|
||||
maxConnections: route.security.maxConnections,
|
||||
reason: 'route-limit',
|
||||
component: 'route-handler'
|
||||
},
|
||||
`route-limit-${route.name}`
|
||||
);
|
||||
socket.end();
|
||||
this.smartProxy.connectionManager.cleanupConnection(record, 'route_connection_limit');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -642,6 +665,10 @@ export class RouteConnectionHandler {
|
||||
|
||||
// Store the route config in the connection record for metrics and other uses
|
||||
record.routeConfig = route;
|
||||
record.routeId = route.id || route.name || 'unnamed';
|
||||
|
||||
// Track connection by route
|
||||
this.smartProxy.connectionManager.trackConnectionByRoute(record.routeId, record.id);
|
||||
|
||||
// Check if this route uses NFTables for forwarding
|
||||
if (action.forwardingEngine === 'nftables') {
|
||||
@ -960,6 +987,10 @@ export class RouteConnectionHandler {
|
||||
|
||||
// Store the route config in the connection record for metrics and other uses
|
||||
record.routeConfig = route;
|
||||
record.routeId = route.id || route.name || 'unnamed';
|
||||
|
||||
// Track connection by route
|
||||
this.smartProxy.connectionManager.trackConnectionByRoute(record.routeId, record.id);
|
||||
|
||||
if (!route.action.socketHandler) {
|
||||
logger.log('error', 'socket-handler action missing socketHandler function', {
|
||||
|
@ -1,5 +1,7 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* Handles security aspects like IP tracking, rate limiting, and authorization
|
||||
@ -7,8 +9,12 @@ import type { SmartProxy } from './smart-proxy.js';
|
||||
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) {}
|
||||
constructor(private smartProxy: SmartProxy) {
|
||||
// Start periodic cleanup every 60 seconds
|
||||
this.startPeriodicCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connections count by IP
|
||||
@ -164,7 +170,76 @@ export class SecurityManager {
|
||||
* 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'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { logger } from '../../core/utils/logger.js';
|
||||
import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
|
||||
|
||||
// Importing required components
|
||||
import { ConnectionManager } from './connection-manager.js';
|
||||
@ -515,6 +516,9 @@ export class SmartProxy extends plugins.EventEmitter {
|
||||
|
||||
// Stop metrics collector
|
||||
this.metricsCollector.stop();
|
||||
|
||||
// Flush any pending deduplicated logs
|
||||
connectionLogDeduplicator.flushAll();
|
||||
|
||||
logger.log('info', 'SmartProxy shutdown complete.');
|
||||
}
|
||||
|
Reference in New Issue
Block a user