@push.rocks/smartproxy 19.6.13 → 19.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist_ts/core/utils/log-deduplicator.d.ts +39 -0
  2. package/dist_ts/core/utils/log-deduplicator.js +297 -0
  3. package/dist_ts/core/utils/shared-security-manager.d.ts +2 -1
  4. package/dist_ts/core/utils/shared-security-manager.js +22 -2
  5. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +1 -0
  6. package/dist_ts/proxies/http-proxy/http-proxy.js +94 -9
  7. package/dist_ts/proxies/http-proxy/models/types.d.ts +2 -0
  8. package/dist_ts/proxies/http-proxy/models/types.js +1 -1
  9. package/dist_ts/proxies/http-proxy/security-manager.d.ts +42 -1
  10. package/dist_ts/proxies/http-proxy/security-manager.js +121 -2
  11. package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +14 -0
  12. package/dist_ts/proxies/smart-proxy/connection-manager.js +86 -32
  13. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +5 -1
  14. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +1 -0
  15. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +25 -9
  16. package/dist_ts/proxies/smart-proxy/security-manager.d.ts +9 -0
  17. package/dist_ts/proxies/smart-proxy/security-manager.js +63 -1
  18. package/dist_ts/proxies/smart-proxy/smart-proxy.js +4 -1
  19. package/package.json +1 -1
  20. package/readme.hints.md +113 -1
  21. package/readme.plan.md +34 -353
  22. package/ts/core/utils/log-deduplicator.ts +361 -0
  23. package/ts/core/utils/shared-security-manager.ts +24 -1
  24. package/ts/proxies/http-proxy/http-proxy.ts +129 -9
  25. package/ts/proxies/http-proxy/models/types.ts +4 -0
  26. package/ts/proxies/http-proxy/security-manager.ts +136 -1
  27. package/ts/proxies/smart-proxy/connection-manager.ts +113 -36
  28. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +5 -0
  29. package/ts/proxies/smart-proxy/models/interfaces.ts +1 -0
  30. package/ts/proxies/smart-proxy/route-connection-handler.ts +52 -15
  31. package/ts/proxies/smart-proxy/security-manager.ts +76 -1
  32. package/ts/proxies/smart-proxy/smart-proxy.ts +4 -0
@@ -14,7 +14,14 @@ export class SecurityManager {
14
14
  // Store rate limits per route and key
15
15
  private rateLimits: Map<string, Map<string, { count: number, expiry: number }>> = new Map();
16
16
 
17
- constructor(private logger: ILogger, private routes: IRouteConfig[] = []) {}
17
+ // Connection tracking by IP
18
+ private connectionsByIP: Map<string, Set<string>> = new Map();
19
+ private connectionRateByIP: Map<string, number[]> = new Map();
20
+
21
+ constructor(private logger: ILogger, private routes: IRouteConfig[] = [], private maxConnectionsPerIP: number = 100, private connectionRateLimitPerMinute: number = 300) {
22
+ // Start periodic cleanup for connection tracking
23
+ this.startPeriodicIpCleanup();
24
+ }
18
25
 
19
26
  /**
20
27
  * Update the routes configuration
@@ -295,4 +302,132 @@ export class SecurityManager {
295
302
  return false;
296
303
  }
297
304
  }
305
+
306
+ /**
307
+ * Get connections count by IP
308
+ */
309
+ public getConnectionCountByIP(ip: string): number {
310
+ return this.connectionsByIP.get(ip)?.size || 0;
311
+ }
312
+
313
+ /**
314
+ * Check and update connection rate for an IP
315
+ * @returns true if within rate limit, false if exceeding limit
316
+ */
317
+ public checkConnectionRate(ip: string): boolean {
318
+ const now = Date.now();
319
+ const minute = 60 * 1000;
320
+
321
+ if (!this.connectionRateByIP.has(ip)) {
322
+ this.connectionRateByIP.set(ip, [now]);
323
+ return true;
324
+ }
325
+
326
+ // Get timestamps and filter out entries older than 1 minute
327
+ const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
328
+ timestamps.push(now);
329
+ this.connectionRateByIP.set(ip, timestamps);
330
+
331
+ // Check if rate exceeds limit
332
+ return timestamps.length <= this.connectionRateLimitPerMinute;
333
+ }
334
+
335
+ /**
336
+ * Track connection by IP
337
+ */
338
+ public trackConnectionByIP(ip: string, connectionId: string): void {
339
+ if (!this.connectionsByIP.has(ip)) {
340
+ this.connectionsByIP.set(ip, new Set());
341
+ }
342
+ this.connectionsByIP.get(ip)!.add(connectionId);
343
+ }
344
+
345
+ /**
346
+ * Remove connection tracking for an IP
347
+ */
348
+ public removeConnectionByIP(ip: string, connectionId: string): void {
349
+ if (this.connectionsByIP.has(ip)) {
350
+ const connections = this.connectionsByIP.get(ip)!;
351
+ connections.delete(connectionId);
352
+ if (connections.size === 0) {
353
+ this.connectionsByIP.delete(ip);
354
+ }
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Check if IP should be allowed considering connection rate and max connections
360
+ * @returns Object with result and reason
361
+ */
362
+ public validateIP(ip: string): { allowed: boolean; reason?: string } {
363
+ // Check connection count limit
364
+ if (this.getConnectionCountByIP(ip) >= this.maxConnectionsPerIP) {
365
+ return {
366
+ allowed: false,
367
+ reason: `Maximum connections per IP (${this.maxConnectionsPerIP}) exceeded`
368
+ };
369
+ }
370
+
371
+ // Check connection rate limit
372
+ if (!this.checkConnectionRate(ip)) {
373
+ return {
374
+ allowed: false,
375
+ reason: `Connection rate limit (${this.connectionRateLimitPerMinute}/min) exceeded`
376
+ };
377
+ }
378
+
379
+ return { allowed: true };
380
+ }
381
+
382
+ /**
383
+ * Clears all IP tracking data (for shutdown)
384
+ */
385
+ public clearIPTracking(): void {
386
+ this.connectionsByIP.clear();
387
+ this.connectionRateByIP.clear();
388
+ }
389
+
390
+ /**
391
+ * Start periodic cleanup of IP tracking data
392
+ */
393
+ private startPeriodicIpCleanup(): void {
394
+ // Clean up IP tracking data every minute
395
+ setInterval(() => {
396
+ this.performIpCleanup();
397
+ }, 60000).unref();
398
+ }
399
+
400
+ /**
401
+ * Perform cleanup of expired IP data
402
+ */
403
+ private performIpCleanup(): void {
404
+ const now = Date.now();
405
+ const minute = 60 * 1000;
406
+ let cleanedRateLimits = 0;
407
+ let cleanedIPs = 0;
408
+
409
+ // Clean up expired rate limit timestamps
410
+ for (const [ip, timestamps] of this.connectionRateByIP.entries()) {
411
+ const validTimestamps = timestamps.filter(time => now - time < minute);
412
+
413
+ if (validTimestamps.length === 0) {
414
+ this.connectionRateByIP.delete(ip);
415
+ cleanedRateLimits++;
416
+ } else if (validTimestamps.length < timestamps.length) {
417
+ this.connectionRateByIP.set(ip, validTimestamps);
418
+ }
419
+ }
420
+
421
+ // Clean up IPs with no active connections
422
+ for (const [ip, connections] of this.connectionsByIP.entries()) {
423
+ if (connections.size === 0) {
424
+ this.connectionsByIP.delete(ip);
425
+ cleanedIPs++;
426
+ }
427
+ }
428
+
429
+ if (cleanedRateLimits > 0 || cleanedIPs > 0) {
430
+ this.logger.debug(`IP cleanup: removed ${cleanedIPs} IPs and ${cleanedRateLimits} rate limits`);
431
+ }
432
+ }
298
433
  }
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { IConnectionRecord } from './models/interfaces.js';
3
3
  import { logger } from '../../core/utils/logger.js';
4
+ import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
4
5
  import { LifecycleComponent } from '../../core/utils/lifecycle-component.js';
5
6
  import { cleanupSocket } from '../../core/utils/socket-utils.js';
6
7
  import { WrappedSocket } from '../../core/models/wrapped-socket.js';
@@ -26,6 +27,10 @@ export class ConnectionManager extends LifecycleComponent {
26
27
  // Cleanup queue for batched processing
27
28
  private cleanupQueue: Set<string> = new Set();
28
29
  private cleanupTimer: NodeJS.Timeout | null = null;
30
+ private isProcessingCleanup: boolean = false;
31
+
32
+ // Route-level connection tracking
33
+ private connectionsByRoute: Map<string, Set<string>> = new Map();
29
34
 
30
35
  constructor(
31
36
  private smartProxy: SmartProxy
@@ -56,11 +61,19 @@ export class ConnectionManager extends LifecycleComponent {
56
61
  public createConnection(socket: plugins.net.Socket | WrappedSocket): IConnectionRecord | null {
57
62
  // Enforce connection limit
58
63
  if (this.connectionRecords.size >= this.maxConnections) {
59
- logger.log('warn', `Connection limit reached (${this.maxConnections}). Rejecting new connection.`, {
60
- currentConnections: this.connectionRecords.size,
61
- maxConnections: this.maxConnections,
62
- component: 'connection-manager'
63
- });
64
+ // Use deduplicated logging for connection limit
65
+ connectionLogDeduplicator.log(
66
+ 'connection-rejected',
67
+ 'warn',
68
+ 'Global connection limit reached',
69
+ {
70
+ reason: 'global-limit',
71
+ currentConnections: this.connectionRecords.size,
72
+ maxConnections: this.maxConnections,
73
+ component: 'connection-manager'
74
+ },
75
+ 'global-limit'
76
+ );
64
77
  socket.destroy();
65
78
  return null;
66
79
  }
@@ -165,18 +178,53 @@ export class ConnectionManager extends LifecycleComponent {
165
178
  return this.connectionRecords.size;
166
179
  }
167
180
 
181
+ /**
182
+ * Track connection by route
183
+ */
184
+ public trackConnectionByRoute(routeId: string, connectionId: string): void {
185
+ if (!this.connectionsByRoute.has(routeId)) {
186
+ this.connectionsByRoute.set(routeId, new Set());
187
+ }
188
+ this.connectionsByRoute.get(routeId)!.add(connectionId);
189
+ }
190
+
191
+ /**
192
+ * Remove connection tracking for a route
193
+ */
194
+ public removeConnectionByRoute(routeId: string, connectionId: string): void {
195
+ if (this.connectionsByRoute.has(routeId)) {
196
+ const connections = this.connectionsByRoute.get(routeId)!;
197
+ connections.delete(connectionId);
198
+ if (connections.size === 0) {
199
+ this.connectionsByRoute.delete(routeId);
200
+ }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Get connection count by route
206
+ */
207
+ public getConnectionCountByRoute(routeId: string): number {
208
+ return this.connectionsByRoute.get(routeId)?.size || 0;
209
+ }
210
+
168
211
  /**
169
212
  * Initiates cleanup once for a connection
170
213
  */
171
214
  public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
172
- if (this.smartProxy.settings.enableDetailedLogging) {
173
- logger.log('info', `Connection cleanup initiated`, {
215
+ // Use deduplicated logging for cleanup events
216
+ connectionLogDeduplicator.log(
217
+ 'connection-cleanup',
218
+ 'info',
219
+ `Connection cleanup: ${reason}`,
220
+ {
174
221
  connectionId: record.id,
175
222
  remoteIP: record.remoteIP,
176
223
  reason,
177
224
  component: 'connection-manager'
178
- });
179
- }
225
+ },
226
+ reason
227
+ );
180
228
 
181
229
  if (record.incomingTerminationReason == null) {
182
230
  record.incomingTerminationReason = reason;
@@ -200,10 +248,10 @@ export class ConnectionManager extends LifecycleComponent {
200
248
 
201
249
  this.cleanupQueue.add(connectionId);
202
250
 
203
- // Process immediately if queue is getting large
204
- if (this.cleanupQueue.size >= this.cleanupBatchSize) {
251
+ // Process immediately if queue is getting large and not already processing
252
+ if (this.cleanupQueue.size >= this.cleanupBatchSize && !this.isProcessingCleanup) {
205
253
  this.processCleanupQueue();
206
- } else if (!this.cleanupTimer) {
254
+ } else if (!this.cleanupTimer && !this.isProcessingCleanup) {
207
255
  // Otherwise, schedule batch processing
208
256
  this.cleanupTimer = this.setTimeout(() => {
209
257
  this.processCleanupQueue();
@@ -215,27 +263,40 @@ export class ConnectionManager extends LifecycleComponent {
215
263
  * Process the cleanup queue in batches
216
264
  */
217
265
  private processCleanupQueue(): void {
266
+ // Prevent concurrent processing
267
+ if (this.isProcessingCleanup) {
268
+ return;
269
+ }
270
+
271
+ this.isProcessingCleanup = true;
272
+
218
273
  if (this.cleanupTimer) {
219
274
  this.clearTimeout(this.cleanupTimer);
220
275
  this.cleanupTimer = null;
221
276
  }
222
277
 
223
- const toCleanup = Array.from(this.cleanupQueue).slice(0, this.cleanupBatchSize);
224
-
225
- // Remove only the items we're processing, not the entire queue!
226
- for (const connectionId of toCleanup) {
227
- this.cleanupQueue.delete(connectionId);
228
- const record = this.connectionRecords.get(connectionId);
229
- if (record) {
230
- this.cleanupConnection(record, record.incomingTerminationReason || 'normal');
278
+ try {
279
+ // Take a snapshot of items to process
280
+ const toCleanup = Array.from(this.cleanupQueue).slice(0, this.cleanupBatchSize);
281
+
282
+ // Remove only the items we're processing from the queue
283
+ for (const connectionId of toCleanup) {
284
+ this.cleanupQueue.delete(connectionId);
285
+ const record = this.connectionRecords.get(connectionId);
286
+ if (record) {
287
+ this.cleanupConnection(record, record.incomingTerminationReason || 'normal');
288
+ }
289
+ }
290
+ } finally {
291
+ // Always reset the processing flag
292
+ this.isProcessingCleanup = false;
293
+
294
+ // Check if more items were added while we were processing
295
+ if (this.cleanupQueue.size > 0) {
296
+ this.cleanupTimer = this.setTimeout(() => {
297
+ this.processCleanupQueue();
298
+ }, 10);
231
299
  }
232
- }
233
-
234
- // If there are more in queue, schedule next batch
235
- if (this.cleanupQueue.size > 0) {
236
- this.cleanupTimer = this.setTimeout(() => {
237
- this.processCleanupQueue();
238
- }, 10);
239
300
  }
240
301
  }
241
302
 
@@ -252,6 +313,11 @@ export class ConnectionManager extends LifecycleComponent {
252
313
  // Track connection termination
253
314
  this.smartProxy.securityManager.removeConnectionByIP(record.remoteIP, record.id);
254
315
 
316
+ // Remove from route tracking
317
+ if (record.routeId) {
318
+ this.removeConnectionByRoute(record.routeId, record.id);
319
+ }
320
+
255
321
  // Remove from metrics tracking
256
322
  if (this.smartProxy.metricsCollector) {
257
323
  this.smartProxy.metricsCollector.removeConnection(record.id);
@@ -335,23 +401,34 @@ export class ConnectionManager extends LifecycleComponent {
335
401
  // Remove the record from the tracking map
336
402
  this.connectionRecords.delete(record.id);
337
403
 
338
- // Log connection details
404
+ // Use deduplicated logging for connection termination
339
405
  if (this.smartProxy.settings.enableDetailedLogging) {
340
- logger.log('info',
341
- `Connection terminated: ${record.remoteIP}:${record.localPort} (${reason}) - ` +
342
- `${plugins.prettyMs(duration)}, IN: ${record.bytesReceived}B, OUT: ${record.bytesSent}B`,
343
- logData
406
+ // For detailed logging, include more info but still deduplicate by IP+reason
407
+ connectionLogDeduplicator.log(
408
+ 'connection-terminated',
409
+ 'info',
410
+ `Connection terminated: ${record.remoteIP}:${record.localPort}`,
411
+ {
412
+ ...logData,
413
+ duration_ms: duration,
414
+ bytesIn: record.bytesReceived,
415
+ bytesOut: record.bytesSent
416
+ },
417
+ `${record.remoteIP}-${reason}`
344
418
  );
345
419
  } else {
346
- logger.log('info',
347
- `Connection terminated: ${record.remoteIP} (${reason}). Active: ${this.connectionRecords.size}`,
420
+ // For normal logging, deduplicate by termination reason
421
+ connectionLogDeduplicator.log(
422
+ 'connection-terminated',
423
+ 'info',
424
+ `Connection terminated`,
348
425
  {
349
- connectionId: record.id,
350
426
  remoteIP: record.remoteIP,
351
427
  reason,
352
428
  activeConnections: this.connectionRecords.size,
353
429
  component: 'connection-manager'
354
- }
430
+ },
431
+ reason // Group by termination reason
355
432
  );
356
433
  }
357
434
  }
@@ -121,6 +121,11 @@ export class HttpProxyBridge {
121
121
  proxySocket.on('error', reject);
122
122
  });
123
123
 
124
+ // Send client IP information header first (custom protocol)
125
+ // Format: "CLIENT_IP:<ip>\r\n"
126
+ const clientIPHeader = Buffer.from(`CLIENT_IP:${record.remoteIP}\r\n`);
127
+ proxySocket.write(clientIPHeader);
128
+
124
129
  // Send initial chunk if present
125
130
  if (initialChunk) {
126
131
  // Count the initial chunk bytes
@@ -165,6 +165,7 @@ export interface IConnectionRecord {
165
165
  tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
166
166
  hasReceivedInitialData: boolean; // Whether initial data has been received
167
167
  routeConfig?: IRouteConfig; // Associated route config for this connection
168
+ routeId?: string; // ID of the route this connection is associated with
168
169
 
169
170
  // Target information (for dynamic port/host mapping)
170
171
  targetHost?: string; // Resolved target host
@@ -1,6 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
3
3
  import { logger } from '../../core/utils/logger.js';
4
+ import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
4
5
  // Route checking functions have been removed
5
6
  import type { IRouteConfig, IRouteAction } from './models/route-types.js';
6
7
  import type { IRouteContext } from '../../core/models/route-context.js';
@@ -89,7 +90,13 @@ export class RouteConnectionHandler {
89
90
  // Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed
90
91
  const ipValidation = this.smartProxy.securityManager.validateIP(wrappedSocket.remoteAddress || '');
91
92
  if (!ipValidation.allowed) {
92
- logger.log('warn', `Connection rejected`, { remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' });
93
+ connectionLogDeduplicator.log(
94
+ 'ip-rejected',
95
+ 'warn',
96
+ `Connection rejected from ${wrappedSocket.remoteAddress}`,
97
+ { remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' },
98
+ wrappedSocket.remoteAddress
99
+ );
93
100
  cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
94
101
  return;
95
102
  }
@@ -563,12 +570,20 @@ export class RouteConnectionHandler {
563
570
  );
564
571
 
565
572
  if (!isIPAllowed) {
566
- logger.log('warn', `IP ${remoteIP} blocked by route security for route ${route.name || 'unnamed'} (connection: ${connectionId})`, {
567
- connectionId,
568
- remoteIP,
569
- routeName: route.name || 'unnamed',
570
- component: 'route-handler'
571
- });
573
+ // Deduplicated logging for route IP blocks
574
+ connectionLogDeduplicator.log(
575
+ 'ip-rejected',
576
+ 'warn',
577
+ `IP blocked by route security`,
578
+ {
579
+ connectionId,
580
+ remoteIP,
581
+ routeName: route.name || 'unnamed',
582
+ reason: 'route-ip-blocked',
583
+ component: 'route-handler'
584
+ },
585
+ remoteIP
586
+ );
572
587
  socket.end();
573
588
  this.smartProxy.connectionManager.cleanupConnection(record, 'route_ip_blocked');
574
589
  return;
@@ -577,14 +592,28 @@ export class RouteConnectionHandler {
577
592
 
578
593
  // Check max connections per route
579
594
  if (route.security.maxConnections !== undefined) {
580
- // TODO: Implement per-route connection tracking
581
- // For now, log that this feature is not yet implemented
582
- if (this.smartProxy.settings.enableDetailedLogging) {
583
- logger.log('warn', `Route ${route.name} has maxConnections=${route.security.maxConnections} configured but per-route connection limits are not yet implemented`, {
584
- connectionId,
585
- routeName: route.name,
586
- component: 'route-handler'
587
- });
595
+ const routeId = route.id || route.name || 'unnamed';
596
+ const currentConnections = this.smartProxy.connectionManager.getConnectionCountByRoute(routeId);
597
+
598
+ if (currentConnections >= route.security.maxConnections) {
599
+ // Deduplicated logging for route connection limits
600
+ connectionLogDeduplicator.log(
601
+ 'connection-rejected',
602
+ 'warn',
603
+ `Route connection limit reached`,
604
+ {
605
+ connectionId,
606
+ routeName: route.name,
607
+ currentConnections,
608
+ maxConnections: route.security.maxConnections,
609
+ reason: 'route-limit',
610
+ component: 'route-handler'
611
+ },
612
+ `route-limit-${route.name}`
613
+ );
614
+ socket.end();
615
+ this.smartProxy.connectionManager.cleanupConnection(record, 'route_connection_limit');
616
+ return;
588
617
  }
589
618
  }
590
619
 
@@ -642,6 +671,10 @@ export class RouteConnectionHandler {
642
671
 
643
672
  // Store the route config in the connection record for metrics and other uses
644
673
  record.routeConfig = route;
674
+ record.routeId = route.id || route.name || 'unnamed';
675
+
676
+ // Track connection by route
677
+ this.smartProxy.connectionManager.trackConnectionByRoute(record.routeId, record.id);
645
678
 
646
679
  // Check if this route uses NFTables for forwarding
647
680
  if (action.forwardingEngine === 'nftables') {
@@ -960,6 +993,10 @@ export class RouteConnectionHandler {
960
993
 
961
994
  // Store the route config in the connection record for metrics and other uses
962
995
  record.routeConfig = route;
996
+ record.routeId = route.id || route.name || 'unnamed';
997
+
998
+ // Track connection by route
999
+ this.smartProxy.connectionManager.trackConnectionByRoute(record.routeId, record.id);
963
1000
 
964
1001
  if (!route.action.socketHandler) {
965
1002
  logger.log('error', 'socket-handler action missing socketHandler function', {
@@ -1,5 +1,7 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { SmartProxy } from './smart-proxy.js';
3
+ import { logger } from '../../core/utils/logger.js';
4
+ import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
3
5
 
4
6
  /**
5
7
  * Handles security aspects like IP tracking, rate limiting, and authorization
@@ -7,8 +9,12 @@ import type { SmartProxy } from './smart-proxy.js';
7
9
  export class SecurityManager {
8
10
  private connectionsByIP: Map<string, Set<string>> = new Map();
9
11
  private connectionRateByIP: Map<string, number[]> = new Map();
12
+ private cleanupInterval: NodeJS.Timeout | null = null;
10
13
 
11
- constructor(private smartProxy: SmartProxy) {}
14
+ constructor(private smartProxy: SmartProxy) {
15
+ // Start periodic cleanup every 60 seconds
16
+ this.startPeriodicCleanup();
17
+ }
12
18
 
13
19
  /**
14
20
  * Get connections count by IP
@@ -164,7 +170,76 @@ export class SecurityManager {
164
170
  * Clears all IP tracking data (for shutdown)
165
171
  */
166
172
  public clearIPTracking(): void {
173
+ if (this.cleanupInterval) {
174
+ clearInterval(this.cleanupInterval);
175
+ this.cleanupInterval = null;
176
+ }
167
177
  this.connectionsByIP.clear();
168
178
  this.connectionRateByIP.clear();
169
179
  }
180
+
181
+ /**
182
+ * Start periodic cleanup of expired data
183
+ */
184
+ private startPeriodicCleanup(): void {
185
+ this.cleanupInterval = setInterval(() => {
186
+ this.performCleanup();
187
+ }, 60000); // Run every minute
188
+
189
+ // Unref the timer so it doesn't keep the process alive
190
+ if (this.cleanupInterval.unref) {
191
+ this.cleanupInterval.unref();
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Perform cleanup of expired rate limits and empty IP entries
197
+ */
198
+ private performCleanup(): void {
199
+ const now = Date.now();
200
+ const minute = 60 * 1000;
201
+ let cleanedRateLimits = 0;
202
+ let cleanedIPs = 0;
203
+
204
+ // Clean up expired rate limit timestamps
205
+ for (const [ip, timestamps] of this.connectionRateByIP.entries()) {
206
+ const validTimestamps = timestamps.filter(time => now - time < minute);
207
+
208
+ if (validTimestamps.length === 0) {
209
+ // No valid timestamps, remove the IP entry
210
+ this.connectionRateByIP.delete(ip);
211
+ cleanedRateLimits++;
212
+ } else if (validTimestamps.length < timestamps.length) {
213
+ // Some timestamps expired, update with valid ones
214
+ this.connectionRateByIP.set(ip, validTimestamps);
215
+ }
216
+ }
217
+
218
+ // Clean up IPs with no active connections
219
+ for (const [ip, connections] of this.connectionsByIP.entries()) {
220
+ if (connections.size === 0) {
221
+ this.connectionsByIP.delete(ip);
222
+ cleanedIPs++;
223
+ }
224
+ }
225
+
226
+ // Log cleanup stats if anything was cleaned
227
+ if (cleanedRateLimits > 0 || cleanedIPs > 0) {
228
+ if (this.smartProxy.settings.enableDetailedLogging) {
229
+ connectionLogDeduplicator.log(
230
+ 'ip-cleanup',
231
+ 'debug',
232
+ 'IP tracking cleanup completed',
233
+ {
234
+ cleanedRateLimits,
235
+ cleanedIPs,
236
+ remainingIPs: this.connectionsByIP.size,
237
+ remainingRateLimits: this.connectionRateByIP.size,
238
+ component: 'security-manager'
239
+ },
240
+ 'periodic-cleanup'
241
+ );
242
+ }
243
+ }
244
+ }
170
245
  }
@@ -1,5 +1,6 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { logger } from '../../core/utils/logger.js';
3
+ import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
3
4
 
4
5
  // Importing required components
5
6
  import { ConnectionManager } from './connection-manager.js';
@@ -515,6 +516,9 @@ export class SmartProxy extends plugins.EventEmitter {
515
516
 
516
517
  // Stop metrics collector
517
518
  this.metricsCollector.stop();
519
+
520
+ // Flush any pending deduplicated logs
521
+ connectionLogDeduplicator.flushAll();
518
522
 
519
523
  logger.log('info', 'SmartProxy shutdown complete.');
520
524
  }