@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.
- 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 +181 -44
- 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 +203 -45
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
|
/**
|
|
@@ -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
|
-
|
|
192
|
-
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}`);
|
|
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
|
-
|
|
199
|
-
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}`);
|
|
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
|
-
|
|
209
|
-
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}`);
|
|
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
|
-
|
|
216
|
-
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}`);
|
|
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
|
-
//
|
|
416
|
-
|
|
417
|
-
targetSocket.
|
|
418
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
699
|
-
|
|
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
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
|
|
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
|
-
|
|
871
|
+
} catch (err) {
|
|
872
|
+
console.log(`Error during forced connection destruction for ${id}: ${err}`);
|
|
729
873
|
}
|
|
730
|
-
|
|
731
|
-
|
|
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
|
}
|