@push.rocks/smartproxy 22.4.2 → 22.6.0

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 (72) hide show
  1. package/changelog.md +28 -0
  2. package/dist_rust/rustproxy +0 -0
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/index.d.ts +1 -5
  5. package/dist_ts/index.js +3 -9
  6. package/dist_ts/protocols/common/fragment-handler.js +5 -1
  7. package/dist_ts/proxies/index.d.ts +1 -5
  8. package/dist_ts/proxies/index.js +2 -6
  9. package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
  10. package/dist_ts/proxies/smart-proxy/index.js +7 -13
  11. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
  12. package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
  13. package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
  14. package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
  15. package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
  16. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
  17. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
  18. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
  19. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
  20. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
  21. package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
  22. package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
  23. package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
  24. package/dist_ts/routing/index.d.ts +1 -1
  25. package/dist_ts/routing/index.js +3 -3
  26. package/dist_ts/routing/models/http-types.d.ts +119 -4
  27. package/dist_ts/routing/models/http-types.js +93 -5
  28. package/package.json +1 -1
  29. package/readme.md +470 -219
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/index.ts +4 -12
  32. package/ts/protocols/common/fragment-handler.ts +4 -0
  33. package/ts/proxies/index.ts +1 -9
  34. package/ts/proxies/smart-proxy/index.ts +6 -13
  35. package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
  36. package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
  37. package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
  38. package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
  39. package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
  40. package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
  41. package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
  42. package/ts/routing/index.ts +2 -2
  43. package/ts/routing/models/http-types.ts +147 -4
  44. package/ts/proxies/http-proxy/connection-pool.ts +0 -228
  45. package/ts/proxies/http-proxy/context-creator.ts +0 -145
  46. package/ts/proxies/http-proxy/default-certificates.ts +0 -150
  47. package/ts/proxies/http-proxy/function-cache.ts +0 -279
  48. package/ts/proxies/http-proxy/handlers/index.ts +0 -5
  49. package/ts/proxies/http-proxy/http-proxy.ts +0 -669
  50. package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
  51. package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
  52. package/ts/proxies/http-proxy/index.ts +0 -18
  53. package/ts/proxies/http-proxy/models/http-types.ts +0 -148
  54. package/ts/proxies/http-proxy/models/index.ts +0 -5
  55. package/ts/proxies/http-proxy/models/types.ts +0 -125
  56. package/ts/proxies/http-proxy/request-handler.ts +0 -878
  57. package/ts/proxies/http-proxy/security-manager.ts +0 -413
  58. package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
  59. package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
  60. package/ts/proxies/smart-proxy/cert-store.ts +0 -92
  61. package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
  62. package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
  63. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
  64. package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
  65. package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
  66. package/ts/proxies/smart-proxy/port-manager.ts +0 -358
  67. package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
  68. package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
  69. package/ts/proxies/smart-proxy/security-manager.ts +0 -269
  70. package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
  71. package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
  72. package/ts/proxies/smart-proxy/tls-manager.ts +0 -171
@@ -1,809 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import type { IConnectionRecord } from './models/interfaces.js';
3
- import { logger } from '../../core/utils/logger.js';
4
- import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
5
- import { LifecycleComponent } from '../../core/utils/lifecycle-component.js';
6
- import { cleanupSocket } from '../../core/utils/socket-utils.js';
7
- import { WrappedSocket } from '../../core/models/wrapped-socket.js';
8
- import { ProtocolDetector } from '../../detection/index.js';
9
- import type { SmartProxy } from './smart-proxy.js';
10
-
11
- /**
12
- * Manages connection lifecycle, tracking, and cleanup with performance optimizations
13
- */
14
- export class ConnectionManager extends LifecycleComponent {
15
- private connectionRecords: Map<string, IConnectionRecord> = new Map();
16
- private terminationStats: {
17
- incoming: Record<string, number>;
18
- outgoing: Record<string, number>;
19
- } = { incoming: {}, outgoing: {} };
20
-
21
- // Performance optimization: Track connections needing inactivity check
22
- private nextInactivityCheck: Map<string, number> = new Map();
23
-
24
- // Connection limits
25
- private readonly maxConnections: number;
26
- private readonly cleanupBatchSize: number = 100;
27
-
28
- // Cleanup queue for batched processing
29
- private cleanupQueue: Set<string> = new Set();
30
- private cleanupTimer: NodeJS.Timeout | null = null;
31
- private isProcessingCleanup: boolean = false;
32
-
33
- // Route-level connection tracking
34
- private connectionsByRoute: Map<string, Set<string>> = new Map();
35
-
36
- constructor(
37
- private smartProxy: SmartProxy
38
- ) {
39
- super();
40
-
41
- // Set reasonable defaults for connection limits
42
- this.maxConnections = smartProxy.settings.defaults?.security?.maxConnections || 10000;
43
-
44
- // Start inactivity check timer if not disabled
45
- if (!smartProxy.settings.disableInactivityCheck) {
46
- this.startInactivityCheckTimer();
47
- }
48
- }
49
-
50
- /**
51
- * Generate a unique connection ID
52
- */
53
- public generateConnectionId(): string {
54
- return Math.random().toString(36).substring(2, 15) +
55
- Math.random().toString(36).substring(2, 15);
56
- }
57
-
58
- /**
59
- * Create and track a new connection
60
- * Accepts either a regular net.Socket or a WrappedSocket for transparent PROXY protocol support
61
- *
62
- * @param socket - The socket for the connection
63
- * @param options - Optional configuration
64
- * @param options.connectionId - Pre-generated connection ID (for atomic IP tracking)
65
- * @param options.skipIpTracking - Skip IP tracking (if already done atomically)
66
- */
67
- public createConnection(
68
- socket: plugins.net.Socket | WrappedSocket,
69
- options?: { connectionId?: string; skipIpTracking?: boolean }
70
- ): IConnectionRecord | null {
71
- // Enforce connection limit
72
- if (this.connectionRecords.size >= this.maxConnections) {
73
- // Use deduplicated logging for connection limit
74
- connectionLogDeduplicator.log(
75
- 'connection-rejected',
76
- 'warn',
77
- 'Global connection limit reached',
78
- {
79
- reason: 'global-limit',
80
- currentConnections: this.connectionRecords.size,
81
- maxConnections: this.maxConnections,
82
- component: 'connection-manager'
83
- },
84
- 'global-limit'
85
- );
86
- socket.destroy();
87
- return null;
88
- }
89
-
90
- const connectionId = options?.connectionId || this.generateConnectionId();
91
- const remoteIP = socket.remoteAddress || '';
92
- const remotePort = socket.remotePort || 0;
93
- const localPort = socket.localPort || 0;
94
- const now = Date.now();
95
-
96
- const record: IConnectionRecord = {
97
- id: connectionId,
98
- incoming: socket,
99
- outgoing: null,
100
- incomingStartTime: now,
101
- lastActivity: now,
102
- connectionClosed: false,
103
- pendingData: [],
104
- pendingDataSize: 0,
105
- bytesReceived: 0,
106
- bytesSent: 0,
107
- remoteIP,
108
- remotePort,
109
- localPort,
110
- isTLS: false,
111
- tlsHandshakeComplete: false,
112
- hasReceivedInitialData: false,
113
- hasKeepAlive: false,
114
- incomingTerminationReason: null,
115
- outgoingTerminationReason: null,
116
- usingNetworkProxy: false,
117
- isBrowserConnection: false,
118
- domainSwitches: 0
119
- };
120
-
121
- this.trackConnection(connectionId, record, options?.skipIpTracking);
122
- return record;
123
- }
124
-
125
- /**
126
- * Track an existing connection
127
- * @param connectionId - The connection ID
128
- * @param record - The connection record
129
- * @param skipIpTracking - Skip IP tracking if already done atomically
130
- */
131
- public trackConnection(connectionId: string, record: IConnectionRecord, skipIpTracking?: boolean): void {
132
- this.connectionRecords.set(connectionId, record);
133
- if (!skipIpTracking) {
134
- this.smartProxy.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
135
- }
136
-
137
- // Schedule inactivity check
138
- if (!this.smartProxy.settings.disableInactivityCheck) {
139
- this.scheduleInactivityCheck(connectionId, record);
140
- }
141
- }
142
-
143
- /**
144
- * Schedule next inactivity check for a connection
145
- */
146
- private scheduleInactivityCheck(connectionId: string, record: IConnectionRecord): void {
147
- let timeout = this.smartProxy.settings.inactivityTimeout!;
148
-
149
- if (record.hasKeepAlive) {
150
- if (this.smartProxy.settings.keepAliveTreatment === 'immortal') {
151
- // Don't schedule check for immortal connections
152
- return;
153
- } else if (this.smartProxy.settings.keepAliveTreatment === 'extended') {
154
- const multiplier = this.smartProxy.settings.keepAliveInactivityMultiplier || 6;
155
- timeout = timeout * multiplier;
156
- }
157
- }
158
-
159
- const checkTime = Date.now() + timeout;
160
- this.nextInactivityCheck.set(connectionId, checkTime);
161
- }
162
-
163
- /**
164
- * Start the inactivity check timer
165
- */
166
- private startInactivityCheckTimer(): void {
167
- // Check more frequently (every 10 seconds) to catch zombies and stuck connections faster
168
- this.setInterval(() => {
169
- this.performOptimizedInactivityCheck();
170
- }, 10000);
171
- // Note: LifecycleComponent's setInterval already calls unref()
172
- }
173
-
174
- /**
175
- * Get a connection by ID
176
- */
177
- public getConnection(connectionId: string): IConnectionRecord | undefined {
178
- return this.connectionRecords.get(connectionId);
179
- }
180
-
181
- /**
182
- * Get all active connections
183
- */
184
- public getConnections(): Map<string, IConnectionRecord> {
185
- return this.connectionRecords;
186
- }
187
-
188
- /**
189
- * Get count of active connections
190
- */
191
- public getConnectionCount(): number {
192
- return this.connectionRecords.size;
193
- }
194
-
195
- /**
196
- * Track connection by route
197
- */
198
- public trackConnectionByRoute(routeId: string, connectionId: string): void {
199
- if (!this.connectionsByRoute.has(routeId)) {
200
- this.connectionsByRoute.set(routeId, new Set());
201
- }
202
- this.connectionsByRoute.get(routeId)!.add(connectionId);
203
- }
204
-
205
- /**
206
- * Remove connection tracking for a route
207
- */
208
- public removeConnectionByRoute(routeId: string, connectionId: string): void {
209
- if (this.connectionsByRoute.has(routeId)) {
210
- const connections = this.connectionsByRoute.get(routeId)!;
211
- connections.delete(connectionId);
212
- if (connections.size === 0) {
213
- this.connectionsByRoute.delete(routeId);
214
- }
215
- }
216
- }
217
-
218
- /**
219
- * Get connection count by route
220
- */
221
- public getConnectionCountByRoute(routeId: string): number {
222
- return this.connectionsByRoute.get(routeId)?.size || 0;
223
- }
224
-
225
- /**
226
- * Initiates cleanup once for a connection
227
- */
228
- public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
229
- // Use deduplicated logging for cleanup events
230
- connectionLogDeduplicator.log(
231
- 'connection-cleanup',
232
- 'info',
233
- `Connection cleanup: ${reason}`,
234
- {
235
- connectionId: record.id,
236
- remoteIP: record.remoteIP,
237
- reason,
238
- component: 'connection-manager'
239
- },
240
- reason
241
- );
242
-
243
- if (record.incomingTerminationReason == null) {
244
- record.incomingTerminationReason = reason;
245
- this.incrementTerminationStat('incoming', reason);
246
- }
247
-
248
- // Add to cleanup queue for batched processing
249
- this.queueCleanup(record.id);
250
- }
251
-
252
- /**
253
- * Queue a connection for cleanup
254
- */
255
- private queueCleanup(connectionId: string): void {
256
- // Check if connection is already being processed
257
- const record = this.connectionRecords.get(connectionId);
258
- if (!record || record.connectionClosed) {
259
- // Already cleaned up or doesn't exist, skip
260
- return;
261
- }
262
-
263
- this.cleanupQueue.add(connectionId);
264
-
265
- // Process immediately if queue is getting large and not already processing
266
- if (this.cleanupQueue.size >= this.cleanupBatchSize && !this.isProcessingCleanup) {
267
- this.processCleanupQueue();
268
- } else if (!this.cleanupTimer && !this.isProcessingCleanup) {
269
- // Otherwise, schedule batch processing
270
- this.cleanupTimer = this.setTimeout(() => {
271
- this.processCleanupQueue();
272
- }, 100);
273
- }
274
- }
275
-
276
- /**
277
- * Process the cleanup queue in batches
278
- */
279
- private processCleanupQueue(): void {
280
- // Prevent concurrent processing
281
- if (this.isProcessingCleanup) {
282
- return;
283
- }
284
-
285
- this.isProcessingCleanup = true;
286
-
287
- if (this.cleanupTimer) {
288
- this.clearTimeout(this.cleanupTimer);
289
- this.cleanupTimer = null;
290
- }
291
-
292
- try {
293
- // Take a snapshot of items to process
294
- const toCleanup = Array.from(this.cleanupQueue).slice(0, this.cleanupBatchSize);
295
-
296
- // Remove only the items we're processing from the queue
297
- for (const connectionId of toCleanup) {
298
- this.cleanupQueue.delete(connectionId);
299
- const record = this.connectionRecords.get(connectionId);
300
- if (record) {
301
- this.cleanupConnection(record, record.incomingTerminationReason || 'normal');
302
- }
303
- }
304
- } finally {
305
- // Always reset the processing flag
306
- this.isProcessingCleanup = false;
307
-
308
- // Check if more items were added while we were processing
309
- if (this.cleanupQueue.size > 0) {
310
- this.cleanupTimer = this.setTimeout(() => {
311
- this.processCleanupQueue();
312
- }, 10);
313
- }
314
- }
315
- }
316
-
317
- /**
318
- * Clean up a connection record
319
- */
320
- public cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
321
- if (!record.connectionClosed) {
322
- record.connectionClosed = true;
323
-
324
- // Remove from inactivity check
325
- this.nextInactivityCheck.delete(record.id);
326
-
327
- // Track connection termination
328
- this.smartProxy.securityManager.removeConnectionByIP(record.remoteIP, record.id);
329
-
330
- // Remove from route tracking
331
- if (record.routeId) {
332
- this.removeConnectionByRoute(record.routeId, record.id);
333
- }
334
-
335
- // Remove from metrics tracking
336
- if (this.smartProxy.metricsCollector) {
337
- this.smartProxy.metricsCollector.removeConnection(record.id);
338
- }
339
-
340
- // Clean up protocol detection fragments
341
- const context = ProtocolDetector.createConnectionContext({
342
- sourceIp: record.remoteIP,
343
- sourcePort: record.incoming?.remotePort || 0,
344
- destIp: record.incoming?.localAddress || '',
345
- destPort: record.localPort,
346
- socketId: record.id
347
- });
348
-
349
- // Clean up any pending detection fragments for this connection
350
- ProtocolDetector.cleanupConnection(context);
351
-
352
- if (record.cleanupTimer) {
353
- clearTimeout(record.cleanupTimer);
354
- record.cleanupTimer = undefined;
355
- }
356
-
357
- // Calculate metrics once
358
- const duration = Date.now() - record.incomingStartTime;
359
- const logData = {
360
- connectionId: record.id,
361
- remoteIP: record.remoteIP,
362
- localPort: record.localPort,
363
- reason,
364
- duration: plugins.prettyMs(duration),
365
- bytes: { in: record.bytesReceived, out: record.bytesSent },
366
- tls: record.isTLS,
367
- keepAlive: record.hasKeepAlive,
368
- usingNetworkProxy: record.usingNetworkProxy,
369
- domainSwitches: record.domainSwitches || 0,
370
- component: 'connection-manager'
371
- };
372
-
373
- // Remove all data handlers to make sure we clean up properly
374
- if (record.incoming) {
375
- try {
376
- record.incoming.removeAllListeners('data');
377
- record.renegotiationHandler = undefined;
378
- } catch (err) {
379
- logger.log('error', `Error removing data handlers: ${err}`, {
380
- connectionId: record.id,
381
- error: err,
382
- component: 'connection-manager'
383
- });
384
- }
385
- }
386
-
387
- // Handle socket cleanup - check if sockets are still active
388
- const cleanupPromises: Promise<void>[] = [];
389
-
390
- if (record.incoming) {
391
- // Extract underlying socket if it's a WrappedSocket
392
- const incomingSocket = record.incoming instanceof WrappedSocket ? record.incoming.socket : record.incoming;
393
- if (!record.incoming.writable || record.incoming.destroyed) {
394
- // Socket is not active, clean up immediately
395
- cleanupPromises.push(cleanupSocket(incomingSocket, `${record.id}-incoming`, { immediate: true }));
396
- } else {
397
- // Socket is still active, allow graceful cleanup
398
- cleanupPromises.push(cleanupSocket(incomingSocket, `${record.id}-incoming`, { allowDrain: true, gracePeriod: 5000 }));
399
- }
400
- }
401
-
402
- if (record.outgoing) {
403
- // Extract underlying socket if it's a WrappedSocket
404
- const outgoingSocket = record.outgoing instanceof WrappedSocket ? record.outgoing.socket : record.outgoing;
405
- if (!record.outgoing.writable || record.outgoing.destroyed) {
406
- // Socket is not active, clean up immediately
407
- cleanupPromises.push(cleanupSocket(outgoingSocket, `${record.id}-outgoing`, { immediate: true }));
408
- } else {
409
- // Socket is still active, allow graceful cleanup
410
- cleanupPromises.push(cleanupSocket(outgoingSocket, `${record.id}-outgoing`, { allowDrain: true, gracePeriod: 5000 }));
411
- }
412
- }
413
-
414
- // Wait for cleanup to complete
415
- Promise.all(cleanupPromises).catch(err => {
416
- logger.log('error', `Error during socket cleanup: ${err}`, {
417
- connectionId: record.id,
418
- error: err,
419
- component: 'connection-manager'
420
- });
421
- });
422
-
423
- // Clear pendingData to avoid memory leaks
424
- record.pendingData = [];
425
- record.pendingDataSize = 0;
426
-
427
- // Remove the record from the tracking map
428
- this.connectionRecords.delete(record.id);
429
-
430
- // Use deduplicated logging for connection termination
431
- if (this.smartProxy.settings.enableDetailedLogging) {
432
- // For detailed logging, include more info but still deduplicate by IP+reason
433
- connectionLogDeduplicator.log(
434
- 'connection-terminated',
435
- 'info',
436
- `Connection terminated: ${record.remoteIP}:${record.localPort}`,
437
- {
438
- ...logData,
439
- duration_ms: duration,
440
- bytesIn: record.bytesReceived,
441
- bytesOut: record.bytesSent
442
- },
443
- `${record.remoteIP}-${reason}`
444
- );
445
- } else {
446
- // For normal logging, deduplicate by termination reason
447
- connectionLogDeduplicator.log(
448
- 'connection-terminated',
449
- 'info',
450
- `Connection terminated`,
451
- {
452
- remoteIP: record.remoteIP,
453
- reason,
454
- activeConnections: this.connectionRecords.size,
455
- component: 'connection-manager'
456
- },
457
- reason // Group by termination reason
458
- );
459
- }
460
- }
461
- }
462
-
463
-
464
- /**
465
- * Creates a generic error handler for incoming or outgoing sockets
466
- */
467
- public handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
468
- return (err: Error) => {
469
- const code = (err as any).code;
470
- let reason = 'error';
471
-
472
- const now = Date.now();
473
- const connectionDuration = now - record.incomingStartTime;
474
- const lastActivityAge = now - record.lastActivity;
475
-
476
- // Update activity tracking
477
- if (side === 'incoming') {
478
- record.lastActivity = now;
479
- this.scheduleInactivityCheck(record.id, record);
480
- }
481
-
482
- const errorData = {
483
- connectionId: record.id,
484
- side,
485
- remoteIP: record.remoteIP,
486
- error: err.message,
487
- duration: plugins.prettyMs(connectionDuration),
488
- lastActivity: plugins.prettyMs(lastActivityAge),
489
- component: 'connection-manager'
490
- };
491
-
492
- switch (code) {
493
- case 'ECONNRESET':
494
- reason = 'econnreset';
495
- logger.log('warn', `ECONNRESET on ${side}: ${record.remoteIP}`, errorData);
496
- break;
497
- case 'ETIMEDOUT':
498
- reason = 'etimedout';
499
- logger.log('warn', `ETIMEDOUT on ${side}: ${record.remoteIP}`, errorData);
500
- break;
501
- default:
502
- logger.log('error', `Error on ${side}: ${record.remoteIP} - ${err.message}`, errorData);
503
- }
504
-
505
- if (side === 'incoming' && record.incomingTerminationReason == null) {
506
- record.incomingTerminationReason = reason;
507
- this.incrementTerminationStat('incoming', reason);
508
- } else if (side === 'outgoing' && record.outgoingTerminationReason == null) {
509
- record.outgoingTerminationReason = reason;
510
- this.incrementTerminationStat('outgoing', reason);
511
- }
512
-
513
- this.initiateCleanupOnce(record, reason);
514
- };
515
- }
516
-
517
- /**
518
- * Creates a generic close handler for incoming or outgoing sockets
519
- */
520
- public handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
521
- return () => {
522
- if (this.smartProxy.settings.enableDetailedLogging) {
523
- logger.log('info', `Connection closed on ${side} side`, {
524
- connectionId: record.id,
525
- side,
526
- remoteIP: record.remoteIP,
527
- component: 'connection-manager'
528
- });
529
- }
530
-
531
- if (side === 'incoming' && record.incomingTerminationReason == null) {
532
- record.incomingTerminationReason = 'normal';
533
- this.incrementTerminationStat('incoming', 'normal');
534
- } else if (side === 'outgoing' && record.outgoingTerminationReason == null) {
535
- record.outgoingTerminationReason = 'normal';
536
- this.incrementTerminationStat('outgoing', 'normal');
537
- record.outgoingClosedTime = Date.now();
538
- }
539
-
540
- this.initiateCleanupOnce(record, 'closed_' + side);
541
- };
542
- }
543
-
544
- /**
545
- * Increment termination statistics
546
- */
547
- public incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
548
- this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
549
- }
550
-
551
- /**
552
- * Get termination statistics
553
- */
554
- public getTerminationStats(): { incoming: Record<string, number>; outgoing: Record<string, number> } {
555
- return this.terminationStats;
556
- }
557
-
558
- /**
559
- * Optimized inactivity check - only checks connections that are due
560
- */
561
- private performOptimizedInactivityCheck(): void {
562
- const now = Date.now();
563
- const connectionsToCheck: string[] = [];
564
-
565
- // Find connections that need checking
566
- for (const [connectionId, checkTime] of this.nextInactivityCheck) {
567
- if (checkTime <= now) {
568
- connectionsToCheck.push(connectionId);
569
- }
570
- }
571
-
572
- // Also check ALL connections for zombie state (destroyed sockets but not cleaned up)
573
- // This is critical for proxy chains where sockets can be destroyed without events
574
- for (const [connectionId, record] of this.connectionRecords) {
575
- if (!record.connectionClosed) {
576
- const incomingDestroyed = record.incoming?.destroyed || false;
577
- const outgoingDestroyed = record.outgoing?.destroyed || false;
578
-
579
- // Check for zombie connections: both sockets destroyed but connection not cleaned up
580
- if (incomingDestroyed && outgoingDestroyed) {
581
- logger.log('warn', `Zombie connection detected: ${connectionId} - both sockets destroyed but not cleaned up`, {
582
- connectionId,
583
- remoteIP: record.remoteIP,
584
- age: plugins.prettyMs(now - record.incomingStartTime),
585
- component: 'connection-manager'
586
- });
587
-
588
- // Clean up immediately
589
- this.cleanupConnection(record, 'zombie_cleanup');
590
- continue;
591
- }
592
-
593
- // Check for half-zombie: one socket destroyed
594
- if (incomingDestroyed || outgoingDestroyed) {
595
- const age = now - record.incomingStartTime;
596
- // Use longer grace period for encrypted connections (5 minutes vs 30 seconds)
597
- const gracePeriod = record.isTLS ? 300000 : 30000;
598
-
599
- // Also ensure connection is old enough to avoid premature cleanup
600
- if (age > gracePeriod && age > 10000) {
601
- logger.log('warn', `Half-zombie connection detected: ${connectionId} - ${incomingDestroyed ? 'incoming' : 'outgoing'} destroyed`, {
602
- connectionId,
603
- remoteIP: record.remoteIP,
604
- age: plugins.prettyMs(age),
605
- incomingDestroyed,
606
- outgoingDestroyed,
607
- isTLS: record.isTLS,
608
- gracePeriod: plugins.prettyMs(gracePeriod),
609
- component: 'connection-manager'
610
- });
611
-
612
- // Clean up
613
- this.cleanupConnection(record, 'half_zombie_cleanup');
614
- }
615
- }
616
-
617
- // Check for stuck connections: no data sent back to client
618
- if (!record.connectionClosed && record.outgoing && record.bytesReceived > 0 && record.bytesSent === 0) {
619
- const age = now - record.incomingStartTime;
620
- // Use longer grace period for encrypted connections (5 minutes vs 60 seconds)
621
- const stuckThreshold = record.isTLS ? 300000 : 60000;
622
-
623
- // If connection is older than threshold and no data sent back, likely stuck
624
- if (age > stuckThreshold) {
625
- logger.log('warn', `Stuck connection detected: ${connectionId} - received ${record.bytesReceived} bytes but sent 0 bytes`, {
626
- connectionId,
627
- remoteIP: record.remoteIP,
628
- age: plugins.prettyMs(age),
629
- bytesReceived: record.bytesReceived,
630
- targetHost: record.targetHost,
631
- targetPort: record.targetPort,
632
- isTLS: record.isTLS,
633
- threshold: plugins.prettyMs(stuckThreshold),
634
- component: 'connection-manager'
635
- });
636
-
637
- // Set termination reason and increment stats
638
- if (record.incomingTerminationReason == null) {
639
- record.incomingTerminationReason = 'stuck_no_response';
640
- this.incrementTerminationStat('incoming', 'stuck_no_response');
641
- }
642
-
643
- // Clean up
644
- this.cleanupConnection(record, 'stuck_no_response');
645
- }
646
- }
647
- }
648
- }
649
-
650
- // Process only connections that need checking
651
- for (const connectionId of connectionsToCheck) {
652
- const record = this.connectionRecords.get(connectionId);
653
- if (!record || record.connectionClosed) {
654
- this.nextInactivityCheck.delete(connectionId);
655
- continue;
656
- }
657
-
658
- const inactivityTime = now - record.lastActivity;
659
-
660
- // Use extended timeout for extended-treatment keep-alive connections
661
- let effectiveTimeout = this.smartProxy.settings.inactivityTimeout!;
662
- if (record.hasKeepAlive && this.smartProxy.settings.keepAliveTreatment === 'extended') {
663
- const multiplier = this.smartProxy.settings.keepAliveInactivityMultiplier || 6;
664
- effectiveTimeout = effectiveTimeout * multiplier;
665
- }
666
-
667
- if (inactivityTime > effectiveTimeout) {
668
- // For keep-alive connections, issue a warning first
669
- if (record.hasKeepAlive && !record.inactivityWarningIssued) {
670
- logger.log('warn', `Keep-alive connection inactive: ${record.remoteIP}`, {
671
- connectionId,
672
- remoteIP: record.remoteIP,
673
- inactiveFor: plugins.prettyMs(inactivityTime),
674
- component: 'connection-manager'
675
- });
676
-
677
- record.inactivityWarningIssued = true;
678
-
679
- // Reschedule check for 10 minutes later
680
- this.nextInactivityCheck.set(connectionId, now + 600000);
681
-
682
- // Try to stimulate activity with a probe packet
683
- if (record.outgoing && !record.outgoing.destroyed) {
684
- try {
685
- record.outgoing.write(Buffer.alloc(0));
686
- } catch (err) {
687
- logger.log('error', `Error sending probe packet: ${err}`, {
688
- connectionId,
689
- error: err,
690
- component: 'connection-manager'
691
- });
692
- }
693
- }
694
- } else {
695
- // Close the connection
696
- logger.log('warn', `Closing inactive connection: ${record.remoteIP}`, {
697
- connectionId,
698
- remoteIP: record.remoteIP,
699
- inactiveFor: plugins.prettyMs(inactivityTime),
700
- hasKeepAlive: record.hasKeepAlive,
701
- component: 'connection-manager'
702
- });
703
- this.cleanupConnection(record, 'inactivity');
704
- }
705
- } else {
706
- // Reschedule next check
707
- this.scheduleInactivityCheck(connectionId, record);
708
- }
709
-
710
- // Parity check: if outgoing socket closed and incoming remains active
711
- // Increased from 2 minutes to 30 minutes for long-lived connections
712
- if (
713
- record.outgoingClosedTime &&
714
- !record.incoming.destroyed &&
715
- !record.connectionClosed &&
716
- now - record.outgoingClosedTime > 1800000 // 30 minutes
717
- ) {
718
- // Only close if no data activity for 10 minutes
719
- if (now - record.lastActivity > 600000) {
720
- logger.log('warn', `Parity check failed after extended timeout: ${record.remoteIP}`, {
721
- connectionId,
722
- remoteIP: record.remoteIP,
723
- timeElapsed: plugins.prettyMs(now - record.outgoingClosedTime),
724
- inactiveFor: plugins.prettyMs(now - record.lastActivity),
725
- component: 'connection-manager'
726
- });
727
- this.cleanupConnection(record, 'parity_check');
728
- }
729
- }
730
- }
731
- }
732
-
733
- /**
734
- * Legacy method for backward compatibility
735
- */
736
- public performInactivityCheck(): void {
737
- this.performOptimizedInactivityCheck();
738
- }
739
-
740
- /**
741
- * Clear all connections (for shutdown)
742
- */
743
- public async clearConnections(): Promise<void> {
744
- // Delegate to LifecycleComponent's cleanup
745
- await this.cleanup();
746
- }
747
-
748
- /**
749
- * Override LifecycleComponent's onCleanup method
750
- */
751
- protected async onCleanup(): Promise<void> {
752
-
753
- // Process connections in batches to avoid blocking
754
- const connections = Array.from(this.connectionRecords.values());
755
- const batchSize = 100;
756
- let index = 0;
757
-
758
- const processBatch = () => {
759
- const batch = connections.slice(index, index + batchSize);
760
-
761
- for (const record of batch) {
762
- try {
763
- if (record.cleanupTimer) {
764
- clearTimeout(record.cleanupTimer);
765
- record.cleanupTimer = undefined;
766
- }
767
-
768
- // Immediate destruction using socket-utils
769
- const shutdownPromises: Promise<void>[] = [];
770
-
771
- if (record.incoming) {
772
- const incomingSocket = record.incoming instanceof WrappedSocket ? record.incoming.socket : record.incoming;
773
- shutdownPromises.push(cleanupSocket(incomingSocket, `${record.id}-incoming-shutdown`, { immediate: true }));
774
- }
775
-
776
- if (record.outgoing) {
777
- const outgoingSocket = record.outgoing instanceof WrappedSocket ? record.outgoing.socket : record.outgoing;
778
- shutdownPromises.push(cleanupSocket(outgoingSocket, `${record.id}-outgoing-shutdown`, { immediate: true }));
779
- }
780
-
781
- // Don't wait for shutdown cleanup in this batch processing
782
- Promise.all(shutdownPromises).catch(() => {});
783
- } catch (err) {
784
- logger.log('error', `Error during connection cleanup: ${err}`, {
785
- connectionId: record.id,
786
- error: err,
787
- component: 'connection-manager'
788
- });
789
- }
790
- }
791
-
792
- index += batchSize;
793
-
794
- // Continue with next batch if needed
795
- if (index < connections.length) {
796
- setImmediate(processBatch);
797
- } else {
798
- // Clear all maps
799
- this.connectionRecords.clear();
800
- this.nextInactivityCheck.clear();
801
- this.cleanupQueue.clear();
802
- this.terminationStats = { incoming: {}, outgoing: {} };
803
- }
804
- };
805
-
806
- // Start batch processing
807
- setImmediate(processBatch);
808
- }
809
- }