@push.rocks/smartproxy 3.23.1 → 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.
@@ -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
  /**
@@ -100,6 +107,7 @@ interface IConnectionRecord {
100
107
  cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
101
108
  lastActivity: number; // Last activity timestamp for inactivity detection
102
109
  pendingData: Buffer[]; // Buffer to hold data during connection setup
110
+ pendingDataSize: number; // Track total size of pending data
103
111
  }
104
112
 
105
113
  // Helper: Check if a port falls within any of the given port ranges
@@ -161,6 +169,11 @@ export class PortProxy {
161
169
  targetIP: settingsArg.targetIP || 'localhost',
162
170
  maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
163
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
164
177
  };
165
178
  }
166
179
 
@@ -187,16 +200,29 @@ export class PortProxy {
187
200
  if (!record.incoming.destroyed) {
188
201
  // Try graceful shutdown first, then force destroy after a short timeout
189
202
  record.incoming.end();
190
- setTimeout(() => {
191
- if (record && !record.incoming.destroyed) {
192
- record.incoming.destroy();
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}`);
193
210
  }
194
211
  }, 1000);
212
+
213
+ // Ensure the timeout doesn't block Node from exiting
214
+ if (incomingTimeout.unref) {
215
+ incomingTimeout.unref();
216
+ }
195
217
  }
196
218
  } catch (err) {
197
219
  console.log(`Error closing incoming socket: ${err}`);
198
- if (!record.incoming.destroyed) {
199
- record.incoming.destroy();
220
+ try {
221
+ if (!record.incoming.destroyed) {
222
+ record.incoming.destroy();
223
+ }
224
+ } catch (destroyErr) {
225
+ console.log(`Error destroying incoming socket: ${destroyErr}`);
200
226
  }
201
227
  }
202
228
 
@@ -204,19 +230,36 @@ export class PortProxy {
204
230
  if (record.outgoing && !record.outgoing.destroyed) {
205
231
  // Try graceful shutdown first, then force destroy after a short timeout
206
232
  record.outgoing.end();
207
- setTimeout(() => {
208
- if (record && record.outgoing && !record.outgoing.destroyed) {
209
- record.outgoing.destroy();
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}`);
210
240
  }
211
241
  }, 1000);
242
+
243
+ // Ensure the timeout doesn't block Node from exiting
244
+ if (outgoingTimeout.unref) {
245
+ outgoingTimeout.unref();
246
+ }
212
247
  }
213
248
  } catch (err) {
214
249
  console.log(`Error closing outgoing socket: ${err}`);
215
- if (record.outgoing && !record.outgoing.destroyed) {
216
- record.outgoing.destroy();
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}`);
217
256
  }
218
257
  }
219
258
 
259
+ // Clear pendingData to avoid memory leaks
260
+ record.pendingData = [];
261
+ record.pendingDataSize = 0;
262
+
220
263
  // Remove the record from the tracking map
221
264
  this.connectionRecords.delete(record.id);
222
265
 
@@ -240,6 +283,11 @@ export class PortProxy {
240
283
  }
241
284
 
242
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
+ }
243
291
  // Define a unified connection handler for all listening ports.
244
292
  const connectionHandler = (socket: plugins.net.Socket) => {
245
293
  if (this.isShuttingDown) {
@@ -251,6 +299,10 @@ export class PortProxy {
251
299
  const remoteIP = socket.remoteAddress || '';
252
300
  const localPort = socket.localPort; // The port on which this connection was accepted.
253
301
 
302
+ // Apply socket optimizations
303
+ socket.setNoDelay(this.settings.noDelay);
304
+ socket.setKeepAlive(this.settings.keepAlive, this.settings.keepAliveInitialDelay);
305
+
254
306
  const connectionId = generateConnectionId();
255
307
  const connectionRecord: IConnectionRecord = {
256
308
  id: connectionId,
@@ -259,7 +311,8 @@ export class PortProxy {
259
311
  incomingStartTime: Date.now(),
260
312
  lastActivity: Date.now(),
261
313
  connectionClosed: false,
262
- pendingData: [] // Initialize buffer for pending data
314
+ pendingData: [], // Initialize buffer for pending data
315
+ pendingDataSize: 0 // Initialize buffer size counter
263
316
  };
264
317
  this.connectionRecords.set(connectionId, connectionRecord);
265
318
 
@@ -296,11 +349,15 @@ export class PortProxy {
296
349
  if (this.settings.sniEnabled) {
297
350
  initialTimeout = setTimeout(() => {
298
351
  if (!initialDataReceived) {
299
- 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
+ }
300
357
  socket.end();
301
358
  cleanupOnce();
302
359
  }
303
- }, 5000);
360
+ }, this.settings.initialDataTimeout || 5000);
304
361
  } else {
305
362
  initialDataReceived = true;
306
363
  }
@@ -393,9 +450,23 @@ export class PortProxy {
393
450
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
394
451
  }
395
452
 
453
+ // Pause the incoming socket to prevent buffer overflows
454
+ socket.pause();
455
+
396
456
  // Temporary handler to collect data during connection setup
397
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
398
468
  connectionRecord.pendingData.push(Buffer.from(chunk));
469
+ connectionRecord.pendingDataSize = newSize;
399
470
  this.updateActivity(connectionRecord);
400
471
  };
401
472
 
@@ -405,6 +476,7 @@ export class PortProxy {
405
476
  // Add initial chunk to pending data if present
406
477
  if (initialChunk) {
407
478
  connectionRecord.pendingData.push(Buffer.from(initialChunk));
479
+ connectionRecord.pendingDataSize = initialChunk.length;
408
480
  }
409
481
 
410
482
  // Create the target socket but don't set up piping immediately
@@ -412,11 +484,47 @@ export class PortProxy {
412
484
  connectionRecord.outgoing = targetSocket;
413
485
  connectionRecord.outgoingStartTime = Date.now();
414
486
 
415
- // Setup error handlers immediately
416
- socket.on('error', handleError('incoming'));
417
- targetSocket.on('error', handleError('outgoing'));
418
- socket.on('close', handleClose('incoming'));
487
+ // Apply socket optimizations
488
+ targetSocket.setNoDelay(this.settings.noDelay);
489
+ targetSocket.setKeepAlive(this.settings.keepAlive, this.settings.keepAliveInitialDelay);
490
+
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
+ });
524
+
525
+ // Setup close handler
419
526
  targetSocket.on('close', handleClose('outgoing'));
527
+ socket.on('close', handleClose('incoming'));
420
528
 
421
529
  // Handle timeouts
422
530
  socket.on('timeout', () => {
@@ -442,6 +550,12 @@ export class PortProxy {
442
550
 
443
551
  // Wait for the outgoing connection to be ready before setting up piping
444
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
+
445
559
  // Remove temporary data handler
446
560
  socket.removeListener('data', tempDataHandler);
447
561
 
@@ -454,9 +568,10 @@ export class PortProxy {
454
568
  return initiateCleanupOnce('write_error');
455
569
  }
456
570
 
457
- // Now set up piping for future data
571
+ // Now set up piping for future data and resume the socket
458
572
  socket.pipe(targetSocket);
459
573
  targetSocket.pipe(socket);
574
+ socket.resume(); // Resume the socket after piping is established
460
575
 
461
576
  console.log(
462
577
  `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
@@ -467,6 +582,7 @@ export class PortProxy {
467
582
  // No pending data, so just set up piping
468
583
  socket.pipe(targetSocket);
469
584
  targetSocket.pipe(socket);
585
+ socket.resume(); // Resume the socket after piping is established
470
586
 
471
587
  console.log(
472
588
  `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
@@ -476,6 +592,7 @@ export class PortProxy {
476
592
 
477
593
  // Clear the buffer now that we've processed it
478
594
  connectionRecord.pendingData = [];
595
+ connectionRecord.pendingDataSize = 0;
479
596
 
480
597
  // Set up activity tracking
481
598
  socket.on('data', () => {
@@ -620,6 +737,8 @@ export class PortProxy {
620
737
 
621
738
  // Log active connection count, longest running durations, and run parity checks every 10 seconds.
622
739
  this.connectionLogger = setInterval(() => {
740
+ // Immediately return if shutting down
741
+ if (this.isShuttingDown) return;
623
742
  if (this.isShuttingDown) return;
624
743
 
625
744
  const now = Date.now();
@@ -675,7 +794,16 @@ export class PortProxy {
675
794
  const closeServerPromises: Promise<void>[] = this.netServers.map(
676
795
  server =>
677
796
  new Promise<void>((resolve) => {
678
- server.close(() => resolve());
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
+ });
679
807
  })
680
808
  );
681
809
 
@@ -689,47 +817,77 @@ export class PortProxy {
689
817
  await Promise.all(closeServerPromises);
690
818
  console.log("All servers closed. Cleaning up active connections...");
691
819
 
692
- // Clean up active connections
820
+ // Force destroy all active connections immediately
693
821
  const connectionIds = [...this.connectionRecords.keys()];
694
822
  console.log(`Cleaning up ${connectionIds.length} active connections...`);
695
823
 
824
+ // First pass: End all connections gracefully
696
825
  for (const id of connectionIds) {
697
826
  const record = this.connectionRecords.get(id);
698
- if (record && !record.connectionClosed) {
699
- this.cleanupConnection(record, 'shutdown');
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
+ }
700
846
  }
701
847
  }
702
848
 
703
- // Wait for graceful shutdown or timeout
704
- const shutdownTimeout = this.settings.gracefulShutdownTimeout || 30000;
705
- await new Promise<void>((resolve) => {
706
- const checkInterval = setInterval(() => {
707
- if (this.connectionRecords.size === 0) {
708
- clearInterval(checkInterval);
709
- resolve(); // lets resolve here as early as we reach 0 remaining connections
710
- }
711
- }, 1000);
712
-
713
- // Force resolve after timeout
714
- setTimeout(() => {
715
- clearInterval(checkInterval);
716
- if (this.connectionRecords.size > 0) {
717
- console.log(`Forcing shutdown with ${this.connectionRecords.size} connections still active`);
718
-
719
- // Force destroy any remaining connections
720
- 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();
721
860
  if (!record.incoming.destroyed) {
722
861
  record.incoming.destroy();
723
862
  }
724
- if (record.outgoing && !record.outgoing.destroyed) {
863
+ }
864
+
865
+ if (record.outgoing) {
866
+ record.outgoing.removeAllListeners();
867
+ if (!record.outgoing.destroyed) {
725
868
  record.outgoing.destroy();
726
869
  }
727
870
  }
728
- this.connectionRecords.clear();
871
+ } catch (err) {
872
+ console.log(`Error during forced connection destruction for ${id}: ${err}`);
729
873
  }
730
- resolve();
731
- }, shutdownTimeout);
732
- });
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
+ };
733
891
 
734
892
  console.log("PortProxy shutdown complete.");
735
893
  }