@push.rocks/smartproxy 3.23.0 → 3.24.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.networkproxy.d.ts +90 -8
- package/dist_ts/classes.networkproxy.js +605 -221
- package/dist_ts/classes.portproxy.d.ts +5 -0
- package/dist_ts/classes.portproxy.js +251 -80
- package/package.json +8 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.networkproxy.ts +743 -268
- package/ts/classes.portproxy.ts +285 -84
package/ts/classes.portproxy.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
23
23
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
24
24
|
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
|
|
25
25
|
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
|
|
26
|
+
|
|
27
|
+
// Socket optimization settings
|
|
28
|
+
noDelay?: boolean; // Disable Nagle's algorithm (default: true)
|
|
29
|
+
keepAlive?: boolean; // Enable TCP keepalive (default: true)
|
|
30
|
+
keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
|
|
31
|
+
maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
|
|
32
|
+
initialDataTimeout?: number; // Timeout for initial data/SNI (ms)
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
/**
|
|
@@ -99,6 +106,8 @@ interface IConnectionRecord {
|
|
|
99
106
|
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
|
|
100
107
|
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
|
|
101
108
|
lastActivity: number; // Last activity timestamp for inactivity detection
|
|
109
|
+
pendingData: Buffer[]; // Buffer to hold data during connection setup
|
|
110
|
+
pendingDataSize: number; // Track total size of pending data
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
// Helper: Check if a port falls within any of the given port ranges
|
|
@@ -160,6 +169,11 @@ export class PortProxy {
|
|
|
160
169
|
targetIP: settingsArg.targetIP || 'localhost',
|
|
161
170
|
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
|
|
162
171
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
|
|
172
|
+
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
173
|
+
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
174
|
+
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 60000, // 1 minute
|
|
175
|
+
maxPendingDataSize: settingsArg.maxPendingDataSize || 1024 * 1024, // 1MB
|
|
176
|
+
initialDataTimeout: settingsArg.initialDataTimeout || 5000 // 5 seconds
|
|
163
177
|
};
|
|
164
178
|
}
|
|
165
179
|
|
|
@@ -186,16 +200,29 @@ export class PortProxy {
|
|
|
186
200
|
if (!record.incoming.destroyed) {
|
|
187
201
|
// Try graceful shutdown first, then force destroy after a short timeout
|
|
188
202
|
record.incoming.end();
|
|
189
|
-
setTimeout(() => {
|
|
190
|
-
|
|
191
|
-
record.incoming.
|
|
203
|
+
const incomingTimeout = setTimeout(() => {
|
|
204
|
+
try {
|
|
205
|
+
if (record && !record.incoming.destroyed) {
|
|
206
|
+
record.incoming.destroy();
|
|
207
|
+
}
|
|
208
|
+
} catch (err) {
|
|
209
|
+
console.log(`Error destroying incoming socket: ${err}`);
|
|
192
210
|
}
|
|
193
211
|
}, 1000);
|
|
212
|
+
|
|
213
|
+
// Ensure the timeout doesn't block Node from exiting
|
|
214
|
+
if (incomingTimeout.unref) {
|
|
215
|
+
incomingTimeout.unref();
|
|
216
|
+
}
|
|
194
217
|
}
|
|
195
218
|
} catch (err) {
|
|
196
219
|
console.log(`Error closing incoming socket: ${err}`);
|
|
197
|
-
|
|
198
|
-
record.incoming.
|
|
220
|
+
try {
|
|
221
|
+
if (!record.incoming.destroyed) {
|
|
222
|
+
record.incoming.destroy();
|
|
223
|
+
}
|
|
224
|
+
} catch (destroyErr) {
|
|
225
|
+
console.log(`Error destroying incoming socket: ${destroyErr}`);
|
|
199
226
|
}
|
|
200
227
|
}
|
|
201
228
|
|
|
@@ -203,19 +230,36 @@ export class PortProxy {
|
|
|
203
230
|
if (record.outgoing && !record.outgoing.destroyed) {
|
|
204
231
|
// Try graceful shutdown first, then force destroy after a short timeout
|
|
205
232
|
record.outgoing.end();
|
|
206
|
-
setTimeout(() => {
|
|
207
|
-
|
|
208
|
-
record.outgoing.
|
|
233
|
+
const outgoingTimeout = setTimeout(() => {
|
|
234
|
+
try {
|
|
235
|
+
if (record && record.outgoing && !record.outgoing.destroyed) {
|
|
236
|
+
record.outgoing.destroy();
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.log(`Error destroying outgoing socket: ${err}`);
|
|
209
240
|
}
|
|
210
241
|
}, 1000);
|
|
242
|
+
|
|
243
|
+
// Ensure the timeout doesn't block Node from exiting
|
|
244
|
+
if (outgoingTimeout.unref) {
|
|
245
|
+
outgoingTimeout.unref();
|
|
246
|
+
}
|
|
211
247
|
}
|
|
212
248
|
} catch (err) {
|
|
213
249
|
console.log(`Error closing outgoing socket: ${err}`);
|
|
214
|
-
|
|
215
|
-
record.outgoing.
|
|
250
|
+
try {
|
|
251
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
252
|
+
record.outgoing.destroy();
|
|
253
|
+
}
|
|
254
|
+
} catch (destroyErr) {
|
|
255
|
+
console.log(`Error destroying outgoing socket: ${destroyErr}`);
|
|
216
256
|
}
|
|
217
257
|
}
|
|
218
258
|
|
|
259
|
+
// Clear pendingData to avoid memory leaks
|
|
260
|
+
record.pendingData = [];
|
|
261
|
+
record.pendingDataSize = 0;
|
|
262
|
+
|
|
219
263
|
// Remove the record from the tracking map
|
|
220
264
|
this.connectionRecords.delete(record.id);
|
|
221
265
|
|
|
@@ -239,6 +283,11 @@ export class PortProxy {
|
|
|
239
283
|
}
|
|
240
284
|
|
|
241
285
|
public async start() {
|
|
286
|
+
// Don't start if already shutting down
|
|
287
|
+
if (this.isShuttingDown) {
|
|
288
|
+
console.log("Cannot start PortProxy while it's shutting down");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
242
291
|
// Define a unified connection handler for all listening ports.
|
|
243
292
|
const connectionHandler = (socket: plugins.net.Socket) => {
|
|
244
293
|
if (this.isShuttingDown) {
|
|
@@ -250,6 +299,10 @@ export class PortProxy {
|
|
|
250
299
|
const remoteIP = socket.remoteAddress || '';
|
|
251
300
|
const localPort = socket.localPort; // The port on which this connection was accepted.
|
|
252
301
|
|
|
302
|
+
// Apply socket optimizations
|
|
303
|
+
socket.setNoDelay(this.settings.noDelay);
|
|
304
|
+
socket.setKeepAlive(this.settings.keepAlive, this.settings.keepAliveInitialDelay);
|
|
305
|
+
|
|
253
306
|
const connectionId = generateConnectionId();
|
|
254
307
|
const connectionRecord: IConnectionRecord = {
|
|
255
308
|
id: connectionId,
|
|
@@ -257,7 +310,9 @@ export class PortProxy {
|
|
|
257
310
|
outgoing: null,
|
|
258
311
|
incomingStartTime: Date.now(),
|
|
259
312
|
lastActivity: Date.now(),
|
|
260
|
-
connectionClosed: false
|
|
313
|
+
connectionClosed: false,
|
|
314
|
+
pendingData: [], // Initialize buffer for pending data
|
|
315
|
+
pendingDataSize: 0 // Initialize buffer size counter
|
|
261
316
|
};
|
|
262
317
|
this.connectionRecords.set(connectionId, connectionRecord);
|
|
263
318
|
|
|
@@ -294,11 +349,15 @@ export class PortProxy {
|
|
|
294
349
|
if (this.settings.sniEnabled) {
|
|
295
350
|
initialTimeout = setTimeout(() => {
|
|
296
351
|
if (!initialDataReceived) {
|
|
297
|
-
console.log(`Initial data timeout for ${remoteIP}`);
|
|
352
|
+
console.log(`Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`);
|
|
353
|
+
if (incomingTerminationReason === null) {
|
|
354
|
+
incomingTerminationReason = 'initial_timeout';
|
|
355
|
+
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
356
|
+
}
|
|
298
357
|
socket.end();
|
|
299
358
|
cleanupOnce();
|
|
300
359
|
}
|
|
301
|
-
}, 5000);
|
|
360
|
+
}, this.settings.initialDataTimeout || 5000);
|
|
302
361
|
} else {
|
|
303
362
|
initialDataReceived = true;
|
|
304
363
|
}
|
|
@@ -391,29 +450,83 @@ export class PortProxy {
|
|
|
391
450
|
connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
|
|
392
451
|
}
|
|
393
452
|
|
|
394
|
-
//
|
|
453
|
+
// Pause the incoming socket to prevent buffer overflows
|
|
454
|
+
socket.pause();
|
|
455
|
+
|
|
456
|
+
// Temporary handler to collect data during connection setup
|
|
457
|
+
const tempDataHandler = (chunk: Buffer) => {
|
|
458
|
+
// Check if adding this chunk would exceed the buffer limit
|
|
459
|
+
const newSize = connectionRecord.pendingDataSize + chunk.length;
|
|
460
|
+
|
|
461
|
+
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
|
|
462
|
+
console.log(`Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`);
|
|
463
|
+
socket.end(); // Gracefully close the socket
|
|
464
|
+
return initiateCleanupOnce('buffer_limit_exceeded');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Buffer the chunk and update the size counter
|
|
468
|
+
connectionRecord.pendingData.push(Buffer.from(chunk));
|
|
469
|
+
connectionRecord.pendingDataSize = newSize;
|
|
470
|
+
this.updateActivity(connectionRecord);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// Add the temp handler to capture all incoming data during connection setup
|
|
474
|
+
socket.on('data', tempDataHandler);
|
|
475
|
+
|
|
476
|
+
// Add initial chunk to pending data if present
|
|
477
|
+
if (initialChunk) {
|
|
478
|
+
connectionRecord.pendingData.push(Buffer.from(initialChunk));
|
|
479
|
+
connectionRecord.pendingDataSize = initialChunk.length;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Create the target socket but don't set up piping immediately
|
|
395
483
|
const targetSocket = plugins.net.connect(connectionOptions);
|
|
396
484
|
connectionRecord.outgoing = targetSocket;
|
|
397
485
|
connectionRecord.outgoingStartTime = Date.now();
|
|
398
486
|
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
}
|
|
487
|
+
// Apply socket optimizations
|
|
488
|
+
targetSocket.setNoDelay(this.settings.noDelay);
|
|
489
|
+
targetSocket.setKeepAlive(this.settings.keepAlive, this.settings.keepAliveInitialDelay);
|
|
403
490
|
|
|
404
|
-
|
|
405
|
-
targetSocket.
|
|
491
|
+
// Setup specific error handler for connection phase
|
|
492
|
+
targetSocket.once('error', (err) => {
|
|
493
|
+
// This handler runs only once during the initial connection phase
|
|
494
|
+
const code = (err as any).code;
|
|
495
|
+
console.log(`Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`);
|
|
496
|
+
|
|
497
|
+
// Resume the incoming socket to prevent it from hanging
|
|
498
|
+
socket.resume();
|
|
499
|
+
|
|
500
|
+
if (code === 'ECONNREFUSED') {
|
|
501
|
+
console.log(`Target ${targetHost}:${connectionOptions.port} refused connection`);
|
|
502
|
+
} else if (code === 'ETIMEDOUT') {
|
|
503
|
+
console.log(`Connection to ${targetHost}:${connectionOptions.port} timed out`);
|
|
504
|
+
} else if (code === 'ECONNRESET') {
|
|
505
|
+
console.log(`Connection to ${targetHost}:${connectionOptions.port} was reset`);
|
|
506
|
+
} else if (code === 'EHOSTUNREACH') {
|
|
507
|
+
console.log(`Host ${targetHost} is unreachable`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Clear any existing error handler after connection phase
|
|
511
|
+
targetSocket.removeAllListeners('error');
|
|
512
|
+
|
|
513
|
+
// Re-add the normal error handler for established connections
|
|
514
|
+
targetSocket.on('error', handleError('outgoing'));
|
|
515
|
+
|
|
516
|
+
if (outgoingTerminationReason === null) {
|
|
517
|
+
outgoingTerminationReason = 'connection_failed';
|
|
518
|
+
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Clean up the connection
|
|
522
|
+
initiateCleanupOnce(`connection_failed_${code}`);
|
|
523
|
+
});
|
|
406
524
|
|
|
407
|
-
|
|
408
|
-
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
409
|
-
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
// Add appropriate handlers for connection management
|
|
413
|
-
socket.on('error', handleError('incoming'));
|
|
414
|
-
targetSocket.on('error', handleError('outgoing'));
|
|
415
|
-
socket.on('close', handleClose('incoming'));
|
|
525
|
+
// Setup close handler
|
|
416
526
|
targetSocket.on('close', handleClose('outgoing'));
|
|
527
|
+
socket.on('close', handleClose('incoming'));
|
|
528
|
+
|
|
529
|
+
// Handle timeouts
|
|
417
530
|
socket.on('timeout', () => {
|
|
418
531
|
console.log(`Timeout on incoming side from ${remoteIP}`);
|
|
419
532
|
if (incomingTerminationReason === null) {
|
|
@@ -435,13 +548,81 @@ export class PortProxy {
|
|
|
435
548
|
socket.setTimeout(120000);
|
|
436
549
|
targetSocket.setTimeout(120000);
|
|
437
550
|
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
551
|
+
// Wait for the outgoing connection to be ready before setting up piping
|
|
552
|
+
targetSocket.once('connect', () => {
|
|
553
|
+
// Clear the initial connection error handler
|
|
554
|
+
targetSocket.removeAllListeners('error');
|
|
555
|
+
|
|
556
|
+
// Add the normal error handler for established connections
|
|
557
|
+
targetSocket.on('error', handleError('outgoing'));
|
|
558
|
+
|
|
559
|
+
// Remove temporary data handler
|
|
560
|
+
socket.removeListener('data', tempDataHandler);
|
|
561
|
+
|
|
562
|
+
// Flush all pending data to target
|
|
563
|
+
if (connectionRecord.pendingData.length > 0) {
|
|
564
|
+
const combinedData = Buffer.concat(connectionRecord.pendingData);
|
|
565
|
+
targetSocket.write(combinedData, (err) => {
|
|
566
|
+
if (err) {
|
|
567
|
+
console.log(`Error writing pending data to target: ${err.message}`);
|
|
568
|
+
return initiateCleanupOnce('write_error');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Now set up piping for future data and resume the socket
|
|
572
|
+
socket.pipe(targetSocket);
|
|
573
|
+
targetSocket.pipe(socket);
|
|
574
|
+
socket.resume(); // Resume the socket after piping is established
|
|
575
|
+
|
|
576
|
+
console.log(
|
|
577
|
+
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
578
|
+
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
|
|
579
|
+
);
|
|
580
|
+
});
|
|
581
|
+
} else {
|
|
582
|
+
// No pending data, so just set up piping
|
|
583
|
+
socket.pipe(targetSocket);
|
|
584
|
+
targetSocket.pipe(socket);
|
|
585
|
+
socket.resume(); // Resume the socket after piping is established
|
|
586
|
+
|
|
587
|
+
console.log(
|
|
588
|
+
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
589
|
+
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Clear the buffer now that we've processed it
|
|
594
|
+
connectionRecord.pendingData = [];
|
|
595
|
+
connectionRecord.pendingDataSize = 0;
|
|
596
|
+
|
|
597
|
+
// Set up activity tracking
|
|
598
|
+
socket.on('data', () => {
|
|
599
|
+
connectionRecord.lastActivity = Date.now();
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
targetSocket.on('data', () => {
|
|
603
|
+
connectionRecord.lastActivity = Date.now();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// Add the renegotiation listener (we don't need setImmediate here anymore
|
|
607
|
+
// since we're already in the connect callback)
|
|
608
|
+
if (serverName) {
|
|
609
|
+
socket.on('data', (renegChunk: Buffer) => {
|
|
610
|
+
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
611
|
+
try {
|
|
612
|
+
// Try to extract SNI from potential renegotiation
|
|
613
|
+
const newSNI = extractSNI(renegChunk);
|
|
614
|
+
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
|
|
615
|
+
console.log(`Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
|
|
616
|
+
initiateCleanupOnce('sni_mismatch');
|
|
617
|
+
} else if (newSNI) {
|
|
618
|
+
console.log(`Rehandshake detected with same SNI: ${newSNI}. Allowing.`);
|
|
619
|
+
}
|
|
620
|
+
} catch (err) {
|
|
621
|
+
console.log(`Error processing potential renegotiation: ${err}. Allowing connection to continue.`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
445
626
|
});
|
|
446
627
|
|
|
447
628
|
// Initialize a cleanup timer for max connection lifetime
|
|
@@ -514,27 +695,6 @@ export class PortProxy {
|
|
|
514
695
|
connectionRecord.lockedDomain = serverName;
|
|
515
696
|
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
|
|
516
697
|
|
|
517
|
-
// Delay adding the renegotiation listener until the next tick,
|
|
518
|
-
// so the initial ClientHello is not reprocessed.
|
|
519
|
-
setImmediate(() => {
|
|
520
|
-
socket.on('data', (renegChunk: Buffer) => {
|
|
521
|
-
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
522
|
-
try {
|
|
523
|
-
// Try to extract SNI from potential renegotiation
|
|
524
|
-
const newSNI = extractSNI(renegChunk);
|
|
525
|
-
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
|
|
526
|
-
console.log(`Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
|
|
527
|
-
initiateCleanupOnce('sni_mismatch');
|
|
528
|
-
} else if (newSNI) {
|
|
529
|
-
console.log(`Rehandshake detected with same SNI: ${newSNI}. Allowing.`);
|
|
530
|
-
}
|
|
531
|
-
} catch (err) {
|
|
532
|
-
console.log(`Error processing potential renegotiation: ${err}. Allowing connection to continue.`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
|
|
538
698
|
setupConnection(serverName, chunk);
|
|
539
699
|
});
|
|
540
700
|
} else {
|
|
@@ -577,6 +737,8 @@ export class PortProxy {
|
|
|
577
737
|
|
|
578
738
|
// Log active connection count, longest running durations, and run parity checks every 10 seconds.
|
|
579
739
|
this.connectionLogger = setInterval(() => {
|
|
740
|
+
// Immediately return if shutting down
|
|
741
|
+
if (this.isShuttingDown) return;
|
|
580
742
|
if (this.isShuttingDown) return;
|
|
581
743
|
|
|
582
744
|
const now = Date.now();
|
|
@@ -632,7 +794,16 @@ export class PortProxy {
|
|
|
632
794
|
const closeServerPromises: Promise<void>[] = this.netServers.map(
|
|
633
795
|
server =>
|
|
634
796
|
new Promise<void>((resolve) => {
|
|
635
|
-
server.
|
|
797
|
+
if (!server.listening) {
|
|
798
|
+
resolve();
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
server.close((err) => {
|
|
802
|
+
if (err) {
|
|
803
|
+
console.log(`Error closing server: ${err.message}`);
|
|
804
|
+
}
|
|
805
|
+
resolve();
|
|
806
|
+
});
|
|
636
807
|
})
|
|
637
808
|
);
|
|
638
809
|
|
|
@@ -646,47 +817,77 @@ export class PortProxy {
|
|
|
646
817
|
await Promise.all(closeServerPromises);
|
|
647
818
|
console.log("All servers closed. Cleaning up active connections...");
|
|
648
819
|
|
|
649
|
-
//
|
|
820
|
+
// Force destroy all active connections immediately
|
|
650
821
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
651
822
|
console.log(`Cleaning up ${connectionIds.length} active connections...`);
|
|
652
823
|
|
|
824
|
+
// First pass: End all connections gracefully
|
|
653
825
|
for (const id of connectionIds) {
|
|
654
826
|
const record = this.connectionRecords.get(id);
|
|
655
|
-
if (record
|
|
656
|
-
|
|
827
|
+
if (record) {
|
|
828
|
+
try {
|
|
829
|
+
// Clear any timers
|
|
830
|
+
if (record.cleanupTimer) {
|
|
831
|
+
clearTimeout(record.cleanupTimer);
|
|
832
|
+
record.cleanupTimer = undefined;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// End sockets gracefully
|
|
836
|
+
if (record.incoming && !record.incoming.destroyed) {
|
|
837
|
+
record.incoming.end();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
841
|
+
record.outgoing.end();
|
|
842
|
+
}
|
|
843
|
+
} catch (err) {
|
|
844
|
+
console.log(`Error during graceful connection end for ${id}: ${err}`);
|
|
845
|
+
}
|
|
657
846
|
}
|
|
658
847
|
}
|
|
659
848
|
|
|
660
|
-
//
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
setTimeout(() => {
|
|
672
|
-
clearInterval(checkInterval);
|
|
673
|
-
if (this.connectionRecords.size > 0) {
|
|
674
|
-
console.log(`Forcing shutdown with ${this.connectionRecords.size} connections still active`);
|
|
675
|
-
|
|
676
|
-
// Force destroy any remaining connections
|
|
677
|
-
for (const record of this.connectionRecords.values()) {
|
|
849
|
+
// Short delay to allow graceful ends to process
|
|
850
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
851
|
+
|
|
852
|
+
// Second pass: Force destroy everything
|
|
853
|
+
for (const id of connectionIds) {
|
|
854
|
+
const record = this.connectionRecords.get(id);
|
|
855
|
+
if (record) {
|
|
856
|
+
try {
|
|
857
|
+
// Remove all listeners to prevent memory leaks
|
|
858
|
+
if (record.incoming) {
|
|
859
|
+
record.incoming.removeAllListeners();
|
|
678
860
|
if (!record.incoming.destroyed) {
|
|
679
861
|
record.incoming.destroy();
|
|
680
862
|
}
|
|
681
|
-
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (record.outgoing) {
|
|
866
|
+
record.outgoing.removeAllListeners();
|
|
867
|
+
if (!record.outgoing.destroyed) {
|
|
682
868
|
record.outgoing.destroy();
|
|
683
869
|
}
|
|
684
870
|
}
|
|
685
|
-
|
|
871
|
+
} catch (err) {
|
|
872
|
+
console.log(`Error during forced connection destruction for ${id}: ${err}`);
|
|
686
873
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Clear the connection records map
|
|
878
|
+
this.connectionRecords.clear();
|
|
879
|
+
|
|
880
|
+
// Clear the domain target indices map to prevent memory leaks
|
|
881
|
+
this.domainTargetIndices.clear();
|
|
882
|
+
|
|
883
|
+
// Clear any servers array
|
|
884
|
+
this.netServers = [];
|
|
885
|
+
|
|
886
|
+
// Reset termination stats
|
|
887
|
+
this.terminationStats = {
|
|
888
|
+
incoming: {},
|
|
889
|
+
outgoing: {}
|
|
890
|
+
};
|
|
690
891
|
|
|
691
892
|
console.log("PortProxy shutdown complete.");
|
|
692
893
|
}
|