@push.rocks/smartproxy 3.28.6 → 3.29.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.iptablesproxy.d.ts +79 -7
- package/dist_ts/classes.iptablesproxy.js +662 -67
- package/dist_ts/classes.networkproxy.d.ts +46 -1
- package/dist_ts/classes.networkproxy.js +347 -8
- package/dist_ts/classes.portproxy.d.ts +36 -0
- package/dist_ts/classes.portproxy.js +464 -365
- package/package.json +2 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.iptablesproxy.ts +786 -68
- package/ts/classes.networkproxy.ts +417 -7
- package/ts/classes.portproxy.ts +652 -485
package/ts/classes.portproxy.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { NetworkProxy } from './classes.networkproxy.js';
|
|
2
3
|
|
|
3
4
|
/** Domain configuration with per-domain allowed port ranges */
|
|
4
5
|
export interface IDomainConfig {
|
|
@@ -9,6 +10,10 @@ export interface IDomainConfig {
|
|
|
9
10
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
10
11
|
// Allow domain-specific timeout override
|
|
11
12
|
connectionTimeout?: number; // Connection timeout override (ms)
|
|
13
|
+
|
|
14
|
+
// New properties for NetworkProxy integration
|
|
15
|
+
useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
|
|
16
|
+
networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
|
|
12
17
|
}
|
|
13
18
|
|
|
14
19
|
/** Port proxy settings including global allowed port ranges */
|
|
@@ -54,6 +59,9 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
54
59
|
keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
|
|
55
60
|
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
|
|
56
61
|
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
|
62
|
+
|
|
63
|
+
// New property for NetworkProxy integration
|
|
64
|
+
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
|
|
57
65
|
}
|
|
58
66
|
|
|
59
67
|
/**
|
|
@@ -88,6 +96,10 @@ interface IConnectionRecord {
|
|
|
88
96
|
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
|
|
89
97
|
incomingTerminationReason?: string | null; // Reason for incoming termination
|
|
90
98
|
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
|
99
|
+
|
|
100
|
+
// New field for NetworkProxy tracking
|
|
101
|
+
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
|
102
|
+
networkProxyIndex?: number; // Which NetworkProxy instance is being used
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
/**
|
|
@@ -336,6 +348,9 @@ export class PortProxy {
|
|
|
336
348
|
// Connection tracking by IP for rate limiting
|
|
337
349
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
338
350
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
351
|
+
|
|
352
|
+
// New property to store NetworkProxy instances
|
|
353
|
+
private networkProxies: NetworkProxy[] = [];
|
|
339
354
|
|
|
340
355
|
constructor(settingsArg: IPortProxySettings) {
|
|
341
356
|
// Set reasonable defaults for all settings
|
|
@@ -375,6 +390,506 @@ export class PortProxy {
|
|
|
375
390
|
keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
|
|
376
391
|
extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
377
392
|
};
|
|
393
|
+
|
|
394
|
+
// Store NetworkProxy instances if provided
|
|
395
|
+
this.networkProxies = settingsArg.networkProxies || [];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Forwards a TLS connection to a NetworkProxy for handling
|
|
400
|
+
* @param connectionId - Unique connection identifier
|
|
401
|
+
* @param socket - The incoming client socket
|
|
402
|
+
* @param record - The connection record
|
|
403
|
+
* @param domainConfig - The domain configuration
|
|
404
|
+
* @param initialData - Initial data chunk (TLS ClientHello)
|
|
405
|
+
* @param serverName - SNI hostname (if available)
|
|
406
|
+
*/
|
|
407
|
+
private forwardToNetworkProxy(
|
|
408
|
+
connectionId: string,
|
|
409
|
+
socket: plugins.net.Socket,
|
|
410
|
+
record: IConnectionRecord,
|
|
411
|
+
domainConfig: IDomainConfig,
|
|
412
|
+
initialData: Buffer,
|
|
413
|
+
serverName?: string
|
|
414
|
+
): void {
|
|
415
|
+
// Determine which NetworkProxy to use
|
|
416
|
+
const proxyIndex = domainConfig.networkProxyIndex !== undefined
|
|
417
|
+
? domainConfig.networkProxyIndex
|
|
418
|
+
: 0;
|
|
419
|
+
|
|
420
|
+
// Validate the NetworkProxy index
|
|
421
|
+
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
|
|
422
|
+
console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`);
|
|
423
|
+
// Fall back to direct connection
|
|
424
|
+
return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const networkProxy = this.networkProxies[proxyIndex];
|
|
428
|
+
const proxyPort = networkProxy.getListeningPort();
|
|
429
|
+
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
|
430
|
+
|
|
431
|
+
if (this.settings.enableDetailedLogging) {
|
|
432
|
+
console.log(
|
|
433
|
+
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Create a connection to the NetworkProxy
|
|
438
|
+
const proxySocket = plugins.net.connect({
|
|
439
|
+
host: proxyHost,
|
|
440
|
+
port: proxyPort
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Store the outgoing socket in the record
|
|
444
|
+
record.outgoing = proxySocket;
|
|
445
|
+
record.outgoingStartTime = Date.now();
|
|
446
|
+
record.usingNetworkProxy = true;
|
|
447
|
+
record.networkProxyIndex = proxyIndex;
|
|
448
|
+
|
|
449
|
+
// Set up error handlers
|
|
450
|
+
proxySocket.on('error', (err) => {
|
|
451
|
+
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
|
|
452
|
+
this.cleanupConnection(record, 'network_proxy_connect_error');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// Handle connection to NetworkProxy
|
|
456
|
+
proxySocket.on('connect', () => {
|
|
457
|
+
if (this.settings.enableDetailedLogging) {
|
|
458
|
+
console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// First send the initial data that contains the TLS ClientHello
|
|
462
|
+
proxySocket.write(initialData);
|
|
463
|
+
|
|
464
|
+
// Now set up bidirectional piping between client and NetworkProxy
|
|
465
|
+
socket.pipe(proxySocket);
|
|
466
|
+
proxySocket.pipe(socket);
|
|
467
|
+
|
|
468
|
+
// Setup cleanup handlers
|
|
469
|
+
proxySocket.on('close', () => {
|
|
470
|
+
if (this.settings.enableDetailedLogging) {
|
|
471
|
+
console.log(`[${connectionId}] NetworkProxy connection closed`);
|
|
472
|
+
}
|
|
473
|
+
this.cleanupConnection(record, 'network_proxy_closed');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
socket.on('close', () => {
|
|
477
|
+
if (this.settings.enableDetailedLogging) {
|
|
478
|
+
console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`);
|
|
479
|
+
}
|
|
480
|
+
this.cleanupConnection(record, 'client_closed');
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Update activity on data transfer
|
|
484
|
+
socket.on('data', () => this.updateActivity(record));
|
|
485
|
+
proxySocket.on('data', () => this.updateActivity(record));
|
|
486
|
+
|
|
487
|
+
if (this.settings.enableDetailedLogging) {
|
|
488
|
+
console.log(
|
|
489
|
+
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Sets up a direct connection to the target (original behavior)
|
|
497
|
+
* This is used when NetworkProxy isn't configured or as a fallback
|
|
498
|
+
*/
|
|
499
|
+
private setupDirectConnection(
|
|
500
|
+
connectionId: string,
|
|
501
|
+
socket: plugins.net.Socket,
|
|
502
|
+
record: IConnectionRecord,
|
|
503
|
+
domainConfig: IDomainConfig | undefined,
|
|
504
|
+
serverName?: string,
|
|
505
|
+
initialChunk?: Buffer,
|
|
506
|
+
overridePort?: number
|
|
507
|
+
): void {
|
|
508
|
+
// Existing connection setup logic
|
|
509
|
+
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
|
510
|
+
const connectionOptions: plugins.net.NetConnectOpts = {
|
|
511
|
+
host: targetHost,
|
|
512
|
+
port: overridePort !== undefined ? overridePort : this.settings.toPort,
|
|
513
|
+
};
|
|
514
|
+
if (this.settings.preserveSourceIP) {
|
|
515
|
+
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Pause the incoming socket to prevent buffer overflows
|
|
519
|
+
socket.pause();
|
|
520
|
+
|
|
521
|
+
// Temporary handler to collect data during connection setup
|
|
522
|
+
const tempDataHandler = (chunk: Buffer) => {
|
|
523
|
+
// Track bytes received
|
|
524
|
+
record.bytesReceived += chunk.length;
|
|
525
|
+
|
|
526
|
+
// Check for TLS handshake
|
|
527
|
+
if (!record.isTLS && isTlsHandshake(chunk)) {
|
|
528
|
+
record.isTLS = true;
|
|
529
|
+
|
|
530
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
531
|
+
console.log(
|
|
532
|
+
`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Check if adding this chunk would exceed the buffer limit
|
|
538
|
+
const newSize = record.pendingDataSize + chunk.length;
|
|
539
|
+
|
|
540
|
+
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
|
|
541
|
+
console.log(
|
|
542
|
+
`[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
|
|
543
|
+
);
|
|
544
|
+
socket.end(); // Gracefully close the socket
|
|
545
|
+
return this.initiateCleanupOnce(record, 'buffer_limit_exceeded');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Buffer the chunk and update the size counter
|
|
549
|
+
record.pendingData.push(Buffer.from(chunk));
|
|
550
|
+
record.pendingDataSize = newSize;
|
|
551
|
+
this.updateActivity(record);
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// Add the temp handler to capture all incoming data during connection setup
|
|
555
|
+
socket.on('data', tempDataHandler);
|
|
556
|
+
|
|
557
|
+
// Add initial chunk to pending data if present
|
|
558
|
+
if (initialChunk) {
|
|
559
|
+
record.bytesReceived += initialChunk.length;
|
|
560
|
+
record.pendingData.push(Buffer.from(initialChunk));
|
|
561
|
+
record.pendingDataSize = initialChunk.length;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Create the target socket but don't set up piping immediately
|
|
565
|
+
const targetSocket = plugins.net.connect(connectionOptions);
|
|
566
|
+
record.outgoing = targetSocket;
|
|
567
|
+
record.outgoingStartTime = Date.now();
|
|
568
|
+
|
|
569
|
+
// Apply socket optimizations
|
|
570
|
+
targetSocket.setNoDelay(this.settings.noDelay);
|
|
571
|
+
|
|
572
|
+
// Apply keep-alive settings to the outgoing connection as well
|
|
573
|
+
if (this.settings.keepAlive) {
|
|
574
|
+
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
575
|
+
|
|
576
|
+
// Apply enhanced TCP keep-alive options if enabled
|
|
577
|
+
if (this.settings.enableKeepAliveProbes) {
|
|
578
|
+
try {
|
|
579
|
+
if ('setKeepAliveProbes' in targetSocket) {
|
|
580
|
+
(targetSocket as any).setKeepAliveProbes(10);
|
|
581
|
+
}
|
|
582
|
+
if ('setKeepAliveInterval' in targetSocket) {
|
|
583
|
+
(targetSocket as any).setKeepAliveInterval(1000);
|
|
584
|
+
}
|
|
585
|
+
} catch (err) {
|
|
586
|
+
// Ignore errors - these are optional enhancements
|
|
587
|
+
if (this.settings.enableDetailedLogging) {
|
|
588
|
+
console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Setup specific error handler for connection phase
|
|
595
|
+
targetSocket.once('error', (err) => {
|
|
596
|
+
// This handler runs only once during the initial connection phase
|
|
597
|
+
const code = (err as any).code;
|
|
598
|
+
console.log(
|
|
599
|
+
`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
// Resume the incoming socket to prevent it from hanging
|
|
603
|
+
socket.resume();
|
|
604
|
+
|
|
605
|
+
if (code === 'ECONNREFUSED') {
|
|
606
|
+
console.log(
|
|
607
|
+
`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
|
|
608
|
+
);
|
|
609
|
+
} else if (code === 'ETIMEDOUT') {
|
|
610
|
+
console.log(
|
|
611
|
+
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`
|
|
612
|
+
);
|
|
613
|
+
} else if (code === 'ECONNRESET') {
|
|
614
|
+
console.log(
|
|
615
|
+
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`
|
|
616
|
+
);
|
|
617
|
+
} else if (code === 'EHOSTUNREACH') {
|
|
618
|
+
console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Clear any existing error handler after connection phase
|
|
622
|
+
targetSocket.removeAllListeners('error');
|
|
623
|
+
|
|
624
|
+
// Re-add the normal error handler for established connections
|
|
625
|
+
targetSocket.on('error', this.handleError('outgoing', record));
|
|
626
|
+
|
|
627
|
+
if (record.outgoingTerminationReason === null) {
|
|
628
|
+
record.outgoingTerminationReason = 'connection_failed';
|
|
629
|
+
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Clean up the connection
|
|
633
|
+
this.initiateCleanupOnce(record, `connection_failed_${code}`);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Setup close handler
|
|
637
|
+
targetSocket.on('close', this.handleClose('outgoing', record));
|
|
638
|
+
socket.on('close', this.handleClose('incoming', record));
|
|
639
|
+
|
|
640
|
+
// Handle timeouts with keep-alive awareness
|
|
641
|
+
socket.on('timeout', () => {
|
|
642
|
+
// For keep-alive connections, just log a warning instead of closing
|
|
643
|
+
if (record.hasKeepAlive) {
|
|
644
|
+
console.log(
|
|
645
|
+
`[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
|
|
646
|
+
this.settings.socketTimeout || 3600000
|
|
647
|
+
)}. Connection preserved.`
|
|
648
|
+
);
|
|
649
|
+
// Don't close the connection - just log
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// For non-keep-alive connections, proceed with normal cleanup
|
|
654
|
+
console.log(
|
|
655
|
+
`[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(
|
|
656
|
+
this.settings.socketTimeout || 3600000
|
|
657
|
+
)}`
|
|
658
|
+
);
|
|
659
|
+
if (record.incomingTerminationReason === null) {
|
|
660
|
+
record.incomingTerminationReason = 'timeout';
|
|
661
|
+
this.incrementTerminationStat('incoming', 'timeout');
|
|
662
|
+
}
|
|
663
|
+
this.initiateCleanupOnce(record, 'timeout_incoming');
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
targetSocket.on('timeout', () => {
|
|
667
|
+
// For keep-alive connections, just log a warning instead of closing
|
|
668
|
+
if (record.hasKeepAlive) {
|
|
669
|
+
console.log(
|
|
670
|
+
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
|
|
671
|
+
this.settings.socketTimeout || 3600000
|
|
672
|
+
)}. Connection preserved.`
|
|
673
|
+
);
|
|
674
|
+
// Don't close the connection - just log
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// For non-keep-alive connections, proceed with normal cleanup
|
|
679
|
+
console.log(
|
|
680
|
+
`[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(
|
|
681
|
+
this.settings.socketTimeout || 3600000
|
|
682
|
+
)}`
|
|
683
|
+
);
|
|
684
|
+
if (record.outgoingTerminationReason === null) {
|
|
685
|
+
record.outgoingTerminationReason = 'timeout';
|
|
686
|
+
this.incrementTerminationStat('outgoing', 'timeout');
|
|
687
|
+
}
|
|
688
|
+
this.initiateCleanupOnce(record, 'timeout_outgoing');
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Set appropriate timeouts, or disable for immortal keep-alive connections
|
|
692
|
+
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
693
|
+
// Disable timeouts completely for immortal connections
|
|
694
|
+
socket.setTimeout(0);
|
|
695
|
+
targetSocket.setTimeout(0);
|
|
696
|
+
|
|
697
|
+
if (this.settings.enableDetailedLogging) {
|
|
698
|
+
console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
// Set normal timeouts for other connections
|
|
702
|
+
socket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
|
|
703
|
+
targetSocket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Track outgoing data for bytes counting
|
|
707
|
+
targetSocket.on('data', (chunk: Buffer) => {
|
|
708
|
+
record.bytesSent += chunk.length;
|
|
709
|
+
this.updateActivity(record);
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// Wait for the outgoing connection to be ready before setting up piping
|
|
713
|
+
targetSocket.once('connect', () => {
|
|
714
|
+
// Clear the initial connection error handler
|
|
715
|
+
targetSocket.removeAllListeners('error');
|
|
716
|
+
|
|
717
|
+
// Add the normal error handler for established connections
|
|
718
|
+
targetSocket.on('error', this.handleError('outgoing', record));
|
|
719
|
+
|
|
720
|
+
// Remove temporary data handler
|
|
721
|
+
socket.removeListener('data', tempDataHandler);
|
|
722
|
+
|
|
723
|
+
// Flush all pending data to target
|
|
724
|
+
if (record.pendingData.length > 0) {
|
|
725
|
+
const combinedData = Buffer.concat(record.pendingData);
|
|
726
|
+
targetSocket.write(combinedData, (err) => {
|
|
727
|
+
if (err) {
|
|
728
|
+
console.log(
|
|
729
|
+
`[${connectionId}] Error writing pending data to target: ${err.message}`
|
|
730
|
+
);
|
|
731
|
+
return this.initiateCleanupOnce(record, 'write_error');
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Now set up piping for future data and resume the socket
|
|
735
|
+
socket.pipe(targetSocket);
|
|
736
|
+
targetSocket.pipe(socket);
|
|
737
|
+
socket.resume(); // Resume the socket after piping is established
|
|
738
|
+
|
|
739
|
+
if (this.settings.enableDetailedLogging) {
|
|
740
|
+
console.log(
|
|
741
|
+
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
742
|
+
`${
|
|
743
|
+
serverName
|
|
744
|
+
? ` (SNI: ${serverName})`
|
|
745
|
+
: domainConfig
|
|
746
|
+
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
747
|
+
: ''
|
|
748
|
+
}` +
|
|
749
|
+
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
750
|
+
);
|
|
751
|
+
} else {
|
|
752
|
+
console.log(
|
|
753
|
+
`Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
754
|
+
`${
|
|
755
|
+
serverName
|
|
756
|
+
? ` (SNI: ${serverName})`
|
|
757
|
+
: domainConfig
|
|
758
|
+
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
759
|
+
: ''
|
|
760
|
+
}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
} else {
|
|
765
|
+
// No pending data, so just set up piping
|
|
766
|
+
socket.pipe(targetSocket);
|
|
767
|
+
targetSocket.pipe(socket);
|
|
768
|
+
socket.resume(); // Resume the socket after piping is established
|
|
769
|
+
|
|
770
|
+
if (this.settings.enableDetailedLogging) {
|
|
771
|
+
console.log(
|
|
772
|
+
`[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
773
|
+
`${
|
|
774
|
+
serverName
|
|
775
|
+
? ` (SNI: ${serverName})`
|
|
776
|
+
: domainConfig
|
|
777
|
+
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
778
|
+
: ''
|
|
779
|
+
}` +
|
|
780
|
+
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
781
|
+
);
|
|
782
|
+
} else {
|
|
783
|
+
console.log(
|
|
784
|
+
`Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
785
|
+
`${
|
|
786
|
+
serverName
|
|
787
|
+
? ` (SNI: ${serverName})`
|
|
788
|
+
: domainConfig
|
|
789
|
+
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
790
|
+
: ''
|
|
791
|
+
}`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Clear the buffer now that we've processed it
|
|
797
|
+
record.pendingData = [];
|
|
798
|
+
record.pendingDataSize = 0;
|
|
799
|
+
|
|
800
|
+
// Add the renegotiation listener for SNI validation
|
|
801
|
+
if (serverName) {
|
|
802
|
+
socket.on('data', (renegChunk: Buffer) => {
|
|
803
|
+
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
804
|
+
try {
|
|
805
|
+
// Try to extract SNI from potential renegotiation
|
|
806
|
+
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
807
|
+
if (newSNI && newSNI !== record.lockedDomain) {
|
|
808
|
+
console.log(
|
|
809
|
+
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
|
|
810
|
+
);
|
|
811
|
+
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
812
|
+
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
813
|
+
console.log(
|
|
814
|
+
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
} catch (err) {
|
|
818
|
+
console.log(
|
|
819
|
+
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Set connection timeout with simpler logic
|
|
827
|
+
if (record.cleanupTimer) {
|
|
828
|
+
clearTimeout(record.cleanupTimer);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// For immortal keep-alive connections, skip setting a timeout completely
|
|
832
|
+
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
833
|
+
if (this.settings.enableDetailedLogging) {
|
|
834
|
+
console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
|
|
835
|
+
}
|
|
836
|
+
// No cleanup timer for immortal connections
|
|
837
|
+
}
|
|
838
|
+
// For extended keep-alive connections, use extended timeout
|
|
839
|
+
else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
840
|
+
const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
841
|
+
const safeTimeout = ensureSafeTimeout(extendedTimeout);
|
|
842
|
+
|
|
843
|
+
record.cleanupTimer = setTimeout(() => {
|
|
844
|
+
console.log(
|
|
845
|
+
`[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs(
|
|
846
|
+
extendedTimeout
|
|
847
|
+
)}), forcing cleanup.`
|
|
848
|
+
);
|
|
849
|
+
this.initiateCleanupOnce(record, 'extended_lifetime');
|
|
850
|
+
}, safeTimeout);
|
|
851
|
+
|
|
852
|
+
// Make sure timeout doesn't keep the process alive
|
|
853
|
+
if (record.cleanupTimer.unref) {
|
|
854
|
+
record.cleanupTimer.unref();
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
if (this.settings.enableDetailedLogging) {
|
|
858
|
+
console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
// For standard connections, use normal timeout
|
|
862
|
+
else {
|
|
863
|
+
// Use domain-specific timeout if available, otherwise use default
|
|
864
|
+
const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
|
|
865
|
+
const safeTimeout = ensureSafeTimeout(connectionTimeout);
|
|
866
|
+
|
|
867
|
+
record.cleanupTimer = setTimeout(() => {
|
|
868
|
+
console.log(
|
|
869
|
+
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs(
|
|
870
|
+
connectionTimeout
|
|
871
|
+
)}), forcing cleanup.`
|
|
872
|
+
);
|
|
873
|
+
this.initiateCleanupOnce(record, 'connection_timeout');
|
|
874
|
+
}, safeTimeout);
|
|
875
|
+
|
|
876
|
+
// Make sure timeout doesn't keep the process alive
|
|
877
|
+
if (record.cleanupTimer.unref) {
|
|
878
|
+
record.cleanupTimer.unref();
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Mark TLS handshake as complete for TLS connections
|
|
883
|
+
if (record.isTLS) {
|
|
884
|
+
record.tlsHandshakeComplete = true;
|
|
885
|
+
|
|
886
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
887
|
+
console.log(
|
|
888
|
+
`[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
});
|
|
378
893
|
}
|
|
379
894
|
|
|
380
895
|
/**
|
|
@@ -532,7 +1047,8 @@ export class PortProxy {
|
|
|
532
1047
|
` Duration: ${plugins.prettyMs(
|
|
533
1048
|
duration
|
|
534
1049
|
)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
535
|
-
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
1050
|
+
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
|
|
1051
|
+
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
|
|
536
1052
|
);
|
|
537
1053
|
} else {
|
|
538
1054
|
console.log(
|
|
@@ -583,6 +1099,81 @@ export class PortProxy {
|
|
|
583
1099
|
this.cleanupConnection(record, reason);
|
|
584
1100
|
}
|
|
585
1101
|
|
|
1102
|
+
/**
|
|
1103
|
+
* Creates a generic error handler for incoming or outgoing sockets
|
|
1104
|
+
*/
|
|
1105
|
+
private handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
|
1106
|
+
return (err: Error) => {
|
|
1107
|
+
const code = (err as any).code;
|
|
1108
|
+
let reason = 'error';
|
|
1109
|
+
|
|
1110
|
+
const now = Date.now();
|
|
1111
|
+
const connectionDuration = now - record.incomingStartTime;
|
|
1112
|
+
const lastActivityAge = now - record.lastActivity;
|
|
1113
|
+
|
|
1114
|
+
if (code === 'ECONNRESET') {
|
|
1115
|
+
reason = 'econnreset';
|
|
1116
|
+
console.log(
|
|
1117
|
+
`[${record.id}] ECONNRESET on ${side} side from ${record.remoteIP}: ${
|
|
1118
|
+
err.message
|
|
1119
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
1120
|
+
lastActivityAge
|
|
1121
|
+
)} ago`
|
|
1122
|
+
);
|
|
1123
|
+
} else if (code === 'ETIMEDOUT') {
|
|
1124
|
+
reason = 'etimedout';
|
|
1125
|
+
console.log(
|
|
1126
|
+
`[${record.id}] ETIMEDOUT on ${side} side from ${record.remoteIP}: ${
|
|
1127
|
+
err.message
|
|
1128
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
1129
|
+
lastActivityAge
|
|
1130
|
+
)} ago`
|
|
1131
|
+
);
|
|
1132
|
+
} else {
|
|
1133
|
+
console.log(
|
|
1134
|
+
`[${record.id}] Error on ${side} side from ${record.remoteIP}: ${
|
|
1135
|
+
err.message
|
|
1136
|
+
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
1137
|
+
lastActivityAge
|
|
1138
|
+
)} ago`
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (side === 'incoming' && record.incomingTerminationReason === null) {
|
|
1143
|
+
record.incomingTerminationReason = reason;
|
|
1144
|
+
this.incrementTerminationStat('incoming', reason);
|
|
1145
|
+
} else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
|
|
1146
|
+
record.outgoingTerminationReason = reason;
|
|
1147
|
+
this.incrementTerminationStat('outgoing', reason);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
this.initiateCleanupOnce(record, reason);
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Creates a generic close handler for incoming or outgoing sockets
|
|
1156
|
+
*/
|
|
1157
|
+
private handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
|
1158
|
+
return () => {
|
|
1159
|
+
if (this.settings.enableDetailedLogging) {
|
|
1160
|
+
console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
if (side === 'incoming' && record.incomingTerminationReason === null) {
|
|
1164
|
+
record.incomingTerminationReason = 'normal';
|
|
1165
|
+
this.incrementTerminationStat('incoming', 'normal');
|
|
1166
|
+
} else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
|
|
1167
|
+
record.outgoingTerminationReason = 'normal';
|
|
1168
|
+
this.incrementTerminationStat('outgoing', 'normal');
|
|
1169
|
+
// Record the time when outgoing socket closed.
|
|
1170
|
+
record.outgoingClosedTime = Date.now();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
this.initiateCleanupOnce(record, 'closed_' + side);
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
586
1177
|
/**
|
|
587
1178
|
* Main method to start the proxy
|
|
588
1179
|
*/
|
|
@@ -651,7 +1242,10 @@ export class PortProxy {
|
|
|
651
1242
|
hasReceivedInitialData: false,
|
|
652
1243
|
hasKeepAlive: false, // Will set to true if keep-alive is applied
|
|
653
1244
|
incomingTerminationReason: null,
|
|
654
|
-
outgoingTerminationReason: null
|
|
1245
|
+
outgoingTerminationReason: null,
|
|
1246
|
+
|
|
1247
|
+
// Initialize NetworkProxy tracking fields
|
|
1248
|
+
usingNetworkProxy: false
|
|
655
1249
|
};
|
|
656
1250
|
|
|
657
1251
|
// Apply keep-alive settings if enabled
|
|
@@ -695,28 +1289,12 @@ export class PortProxy {
|
|
|
695
1289
|
}
|
|
696
1290
|
|
|
697
1291
|
let initialDataReceived = false;
|
|
698
|
-
let incomingTerminationReason: string | null = null;
|
|
699
|
-
let outgoingTerminationReason: string | null = null;
|
|
700
|
-
|
|
701
|
-
// Define initiateCleanupOnce for compatibility
|
|
702
|
-
const initiateCleanupOnce = (reason: string = 'normal') => {
|
|
703
|
-
if (this.settings.enableDetailedLogging) {
|
|
704
|
-
console.log(`[${connectionId}] Connection cleanup initiated for ${remoteIP} (${reason})`);
|
|
705
|
-
}
|
|
706
|
-
if (incomingTerminationReason === null) {
|
|
707
|
-
incomingTerminationReason = reason;
|
|
708
|
-
connectionRecord.incomingTerminationReason = reason;
|
|
709
|
-
this.incrementTerminationStat('incoming', reason);
|
|
710
|
-
}
|
|
711
|
-
this.cleanupConnection(connectionRecord, reason);
|
|
712
|
-
};
|
|
713
1292
|
|
|
714
|
-
//
|
|
1293
|
+
// Define helpers for rejecting connections
|
|
715
1294
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
|
716
1295
|
console.log(`[${connectionId}] ${logMessage}`);
|
|
717
1296
|
socket.end();
|
|
718
|
-
if (incomingTerminationReason === null) {
|
|
719
|
-
incomingTerminationReason = reason;
|
|
1297
|
+
if (connectionRecord.incomingTerminationReason === null) {
|
|
720
1298
|
connectionRecord.incomingTerminationReason = reason;
|
|
721
1299
|
this.incrementTerminationStat('incoming', reason);
|
|
722
1300
|
}
|
|
@@ -731,8 +1309,7 @@ export class PortProxy {
|
|
|
731
1309
|
console.log(
|
|
732
1310
|
`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
|
|
733
1311
|
);
|
|
734
|
-
if (incomingTerminationReason === null) {
|
|
735
|
-
incomingTerminationReason = 'initial_timeout';
|
|
1312
|
+
if (connectionRecord.incomingTerminationReason === null) {
|
|
736
1313
|
connectionRecord.incomingTerminationReason = 'initial_timeout';
|
|
737
1314
|
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
738
1315
|
}
|
|
@@ -750,9 +1327,7 @@ export class PortProxy {
|
|
|
750
1327
|
connectionRecord.hasReceivedInitialData = true;
|
|
751
1328
|
}
|
|
752
1329
|
|
|
753
|
-
socket.on('error', (
|
|
754
|
-
console.log(`[${connectionId}] Incoming socket error from ${remoteIP}: ${err.message}`);
|
|
755
|
-
});
|
|
1330
|
+
socket.on('error', this.handleError('incoming', connectionRecord));
|
|
756
1331
|
|
|
757
1332
|
// Track data for bytes counting
|
|
758
1333
|
socket.on('data', (chunk: Buffer) => {
|
|
@@ -773,77 +1348,8 @@ export class PortProxy {
|
|
|
773
1348
|
}
|
|
774
1349
|
});
|
|
775
1350
|
|
|
776
|
-
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
|
777
|
-
const code = (err as any).code;
|
|
778
|
-
let reason = 'error';
|
|
779
|
-
|
|
780
|
-
const now = Date.now();
|
|
781
|
-
const connectionDuration = now - connectionRecord.incomingStartTime;
|
|
782
|
-
const lastActivityAge = now - connectionRecord.lastActivity;
|
|
783
|
-
|
|
784
|
-
if (code === 'ECONNRESET') {
|
|
785
|
-
reason = 'econnreset';
|
|
786
|
-
console.log(
|
|
787
|
-
`[${connectionId}] ECONNRESET on ${side} side from ${remoteIP}: ${
|
|
788
|
-
err.message
|
|
789
|
-
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
790
|
-
lastActivityAge
|
|
791
|
-
)} ago`
|
|
792
|
-
);
|
|
793
|
-
} else if (code === 'ETIMEDOUT') {
|
|
794
|
-
reason = 'etimedout';
|
|
795
|
-
console.log(
|
|
796
|
-
`[${connectionId}] ETIMEDOUT on ${side} side from ${remoteIP}: ${
|
|
797
|
-
err.message
|
|
798
|
-
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
799
|
-
lastActivityAge
|
|
800
|
-
)} ago`
|
|
801
|
-
);
|
|
802
|
-
} else {
|
|
803
|
-
console.log(
|
|
804
|
-
`[${connectionId}] Error on ${side} side from ${remoteIP}: ${
|
|
805
|
-
err.message
|
|
806
|
-
}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
|
|
807
|
-
lastActivityAge
|
|
808
|
-
)} ago`
|
|
809
|
-
);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
813
|
-
incomingTerminationReason = reason;
|
|
814
|
-
connectionRecord.incomingTerminationReason = reason;
|
|
815
|
-
this.incrementTerminationStat('incoming', reason);
|
|
816
|
-
} else if (side === 'outgoing' && outgoingTerminationReason === null) {
|
|
817
|
-
outgoingTerminationReason = reason;
|
|
818
|
-
connectionRecord.outgoingTerminationReason = reason;
|
|
819
|
-
this.incrementTerminationStat('outgoing', reason);
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
initiateCleanupOnce(reason);
|
|
823
|
-
};
|
|
824
|
-
|
|
825
|
-
const handleClose = (side: 'incoming' | 'outgoing') => () => {
|
|
826
|
-
if (this.settings.enableDetailedLogging) {
|
|
827
|
-
console.log(`[${connectionId}] Connection closed on ${side} side from ${remoteIP}`);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
831
|
-
incomingTerminationReason = 'normal';
|
|
832
|
-
connectionRecord.incomingTerminationReason = 'normal';
|
|
833
|
-
this.incrementTerminationStat('incoming', 'normal');
|
|
834
|
-
} else if (side === 'outgoing' && outgoingTerminationReason === null) {
|
|
835
|
-
outgoingTerminationReason = 'normal';
|
|
836
|
-
connectionRecord.outgoingTerminationReason = 'normal';
|
|
837
|
-
this.incrementTerminationStat('outgoing', 'normal');
|
|
838
|
-
// Record the time when outgoing socket closed.
|
|
839
|
-
connectionRecord.outgoingClosedTime = Date.now();
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
initiateCleanupOnce('closed_' + side);
|
|
843
|
-
};
|
|
844
|
-
|
|
845
1351
|
/**
|
|
846
|
-
* Sets up the connection to the target host.
|
|
1352
|
+
* Sets up the connection to the target host or NetworkProxy.
|
|
847
1353
|
* @param serverName - The SNI hostname (unused when forcedDomain is provided).
|
|
848
1354
|
* @param initialChunk - Optional initial data chunk.
|
|
849
1355
|
* @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
|
|
@@ -866,7 +1372,8 @@ export class PortProxy {
|
|
|
866
1372
|
connectionRecord.hasReceivedInitialData = true;
|
|
867
1373
|
|
|
868
1374
|
// Check if this looks like a TLS handshake
|
|
869
|
-
|
|
1375
|
+
const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
|
|
1376
|
+
if (isTlsHandshakeDetected) {
|
|
870
1377
|
connectionRecord.isTLS = true;
|
|
871
1378
|
|
|
872
1379
|
if (this.settings.enableTlsDebugLogging) {
|
|
@@ -911,6 +1418,23 @@ export class PortProxy {
|
|
|
911
1418
|
)}`
|
|
912
1419
|
);
|
|
913
1420
|
}
|
|
1421
|
+
|
|
1422
|
+
// Check if we should forward this to a NetworkProxy
|
|
1423
|
+
if (
|
|
1424
|
+
isTlsHandshakeDetected &&
|
|
1425
|
+
domainConfig.useNetworkProxy === true &&
|
|
1426
|
+
initialChunk &&
|
|
1427
|
+
this.networkProxies.length > 0
|
|
1428
|
+
) {
|
|
1429
|
+
return this.forwardToNetworkProxy(
|
|
1430
|
+
connectionId,
|
|
1431
|
+
socket,
|
|
1432
|
+
connectionRecord,
|
|
1433
|
+
domainConfig,
|
|
1434
|
+
initialChunk,
|
|
1435
|
+
serverName
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
914
1438
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
915
1439
|
if (
|
|
916
1440
|
!isGlobIPAllowed(
|
|
@@ -926,393 +1450,16 @@ export class PortProxy {
|
|
|
926
1450
|
}
|
|
927
1451
|
}
|
|
928
1452
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
socket.pause();
|
|
940
|
-
|
|
941
|
-
// Temporary handler to collect data during connection setup
|
|
942
|
-
const tempDataHandler = (chunk: Buffer) => {
|
|
943
|
-
// Track bytes received
|
|
944
|
-
connectionRecord.bytesReceived += chunk.length;
|
|
945
|
-
|
|
946
|
-
// Check for TLS handshake
|
|
947
|
-
if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
|
|
948
|
-
connectionRecord.isTLS = true;
|
|
949
|
-
|
|
950
|
-
if (this.settings.enableTlsDebugLogging) {
|
|
951
|
-
console.log(
|
|
952
|
-
`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
|
|
953
|
-
);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
// Check if adding this chunk would exceed the buffer limit
|
|
958
|
-
const newSize = connectionRecord.pendingDataSize + chunk.length;
|
|
959
|
-
|
|
960
|
-
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
|
|
961
|
-
console.log(
|
|
962
|
-
`[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
|
|
963
|
-
);
|
|
964
|
-
socket.end(); // Gracefully close the socket
|
|
965
|
-
return initiateCleanupOnce('buffer_limit_exceeded');
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
// Buffer the chunk and update the size counter
|
|
969
|
-
connectionRecord.pendingData.push(Buffer.from(chunk));
|
|
970
|
-
connectionRecord.pendingDataSize = newSize;
|
|
971
|
-
this.updateActivity(connectionRecord);
|
|
972
|
-
};
|
|
973
|
-
|
|
974
|
-
// Add the temp handler to capture all incoming data during connection setup
|
|
975
|
-
socket.on('data', tempDataHandler);
|
|
976
|
-
|
|
977
|
-
// Add initial chunk to pending data if present
|
|
978
|
-
if (initialChunk) {
|
|
979
|
-
connectionRecord.bytesReceived += initialChunk.length;
|
|
980
|
-
connectionRecord.pendingData.push(Buffer.from(initialChunk));
|
|
981
|
-
connectionRecord.pendingDataSize = initialChunk.length;
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// Create the target socket but don't set up piping immediately
|
|
985
|
-
const targetSocket = plugins.net.connect(connectionOptions);
|
|
986
|
-
connectionRecord.outgoing = targetSocket;
|
|
987
|
-
connectionRecord.outgoingStartTime = Date.now();
|
|
988
|
-
|
|
989
|
-
// Apply socket optimizations
|
|
990
|
-
targetSocket.setNoDelay(this.settings.noDelay);
|
|
991
|
-
|
|
992
|
-
// Apply keep-alive settings to the outgoing connection as well
|
|
993
|
-
if (this.settings.keepAlive) {
|
|
994
|
-
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
995
|
-
|
|
996
|
-
// Apply enhanced TCP keep-alive options if enabled
|
|
997
|
-
if (this.settings.enableKeepAliveProbes) {
|
|
998
|
-
try {
|
|
999
|
-
if ('setKeepAliveProbes' in targetSocket) {
|
|
1000
|
-
(targetSocket as any).setKeepAliveProbes(10);
|
|
1001
|
-
}
|
|
1002
|
-
if ('setKeepAliveInterval' in targetSocket) {
|
|
1003
|
-
(targetSocket as any).setKeepAliveInterval(1000);
|
|
1004
|
-
}
|
|
1005
|
-
} catch (err) {
|
|
1006
|
-
// Ignore errors - these are optional enhancements
|
|
1007
|
-
if (this.settings.enableDetailedLogging) {
|
|
1008
|
-
console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// Setup specific error handler for connection phase
|
|
1015
|
-
targetSocket.once('error', (err) => {
|
|
1016
|
-
// This handler runs only once during the initial connection phase
|
|
1017
|
-
const code = (err as any).code;
|
|
1018
|
-
console.log(
|
|
1019
|
-
`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`
|
|
1020
|
-
);
|
|
1021
|
-
|
|
1022
|
-
// Resume the incoming socket to prevent it from hanging
|
|
1023
|
-
socket.resume();
|
|
1024
|
-
|
|
1025
|
-
if (code === 'ECONNREFUSED') {
|
|
1026
|
-
console.log(
|
|
1027
|
-
`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
|
|
1028
|
-
);
|
|
1029
|
-
} else if (code === 'ETIMEDOUT') {
|
|
1030
|
-
console.log(
|
|
1031
|
-
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`
|
|
1032
|
-
);
|
|
1033
|
-
} else if (code === 'ECONNRESET') {
|
|
1034
|
-
console.log(
|
|
1035
|
-
`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`
|
|
1036
|
-
);
|
|
1037
|
-
} else if (code === 'EHOSTUNREACH') {
|
|
1038
|
-
console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// Clear any existing error handler after connection phase
|
|
1042
|
-
targetSocket.removeAllListeners('error');
|
|
1043
|
-
|
|
1044
|
-
// Re-add the normal error handler for established connections
|
|
1045
|
-
targetSocket.on('error', handleError('outgoing'));
|
|
1046
|
-
|
|
1047
|
-
if (outgoingTerminationReason === null) {
|
|
1048
|
-
outgoingTerminationReason = 'connection_failed';
|
|
1049
|
-
connectionRecord.outgoingTerminationReason = 'connection_failed';
|
|
1050
|
-
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
// Clean up the connection
|
|
1054
|
-
initiateCleanupOnce(`connection_failed_${code}`);
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
// Setup close handler
|
|
1058
|
-
targetSocket.on('close', handleClose('outgoing'));
|
|
1059
|
-
socket.on('close', handleClose('incoming'));
|
|
1060
|
-
|
|
1061
|
-
// Handle timeouts with keep-alive awareness
|
|
1062
|
-
socket.on('timeout', () => {
|
|
1063
|
-
// For keep-alive connections, just log a warning instead of closing
|
|
1064
|
-
if (connectionRecord.hasKeepAlive) {
|
|
1065
|
-
console.log(
|
|
1066
|
-
`[${connectionId}] Timeout event on incoming keep-alive connection from ${remoteIP} after ${plugins.prettyMs(
|
|
1067
|
-
this.settings.socketTimeout || 3600000
|
|
1068
|
-
)}. Connection preserved.`
|
|
1069
|
-
);
|
|
1070
|
-
// Don't close the connection - just log
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// For non-keep-alive connections, proceed with normal cleanup
|
|
1075
|
-
console.log(
|
|
1076
|
-
`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(
|
|
1077
|
-
this.settings.socketTimeout || 3600000
|
|
1078
|
-
)}`
|
|
1079
|
-
);
|
|
1080
|
-
if (incomingTerminationReason === null) {
|
|
1081
|
-
incomingTerminationReason = 'timeout';
|
|
1082
|
-
connectionRecord.incomingTerminationReason = 'timeout';
|
|
1083
|
-
this.incrementTerminationStat('incoming', 'timeout');
|
|
1084
|
-
}
|
|
1085
|
-
initiateCleanupOnce('timeout_incoming');
|
|
1086
|
-
});
|
|
1087
|
-
|
|
1088
|
-
targetSocket.on('timeout', () => {
|
|
1089
|
-
// For keep-alive connections, just log a warning instead of closing
|
|
1090
|
-
if (connectionRecord.hasKeepAlive) {
|
|
1091
|
-
console.log(
|
|
1092
|
-
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${remoteIP} after ${plugins.prettyMs(
|
|
1093
|
-
this.settings.socketTimeout || 3600000
|
|
1094
|
-
)}. Connection preserved.`
|
|
1095
|
-
);
|
|
1096
|
-
// Don't close the connection - just log
|
|
1097
|
-
return;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
// For non-keep-alive connections, proceed with normal cleanup
|
|
1101
|
-
console.log(
|
|
1102
|
-
`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(
|
|
1103
|
-
this.settings.socketTimeout || 3600000
|
|
1104
|
-
)}`
|
|
1105
|
-
);
|
|
1106
|
-
if (outgoingTerminationReason === null) {
|
|
1107
|
-
outgoingTerminationReason = 'timeout';
|
|
1108
|
-
connectionRecord.outgoingTerminationReason = 'timeout';
|
|
1109
|
-
this.incrementTerminationStat('outgoing', 'timeout');
|
|
1110
|
-
}
|
|
1111
|
-
initiateCleanupOnce('timeout_outgoing');
|
|
1112
|
-
});
|
|
1113
|
-
|
|
1114
|
-
// Set appropriate timeouts, or disable for immortal keep-alive connections
|
|
1115
|
-
if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
1116
|
-
// Disable timeouts completely for immortal connections
|
|
1117
|
-
socket.setTimeout(0);
|
|
1118
|
-
targetSocket.setTimeout(0);
|
|
1119
|
-
|
|
1120
|
-
if (this.settings.enableDetailedLogging) {
|
|
1121
|
-
console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
|
|
1122
|
-
}
|
|
1123
|
-
} else {
|
|
1124
|
-
// Set normal timeouts for other connections
|
|
1125
|
-
socket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
|
|
1126
|
-
targetSocket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// Track outgoing data for bytes counting
|
|
1130
|
-
targetSocket.on('data', (chunk: Buffer) => {
|
|
1131
|
-
connectionRecord.bytesSent += chunk.length;
|
|
1132
|
-
this.updateActivity(connectionRecord);
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
// Wait for the outgoing connection to be ready before setting up piping
|
|
1136
|
-
targetSocket.once('connect', () => {
|
|
1137
|
-
// Clear the initial connection error handler
|
|
1138
|
-
targetSocket.removeAllListeners('error');
|
|
1139
|
-
|
|
1140
|
-
// Add the normal error handler for established connections
|
|
1141
|
-
targetSocket.on('error', handleError('outgoing'));
|
|
1142
|
-
|
|
1143
|
-
// Remove temporary data handler
|
|
1144
|
-
socket.removeListener('data', tempDataHandler);
|
|
1145
|
-
|
|
1146
|
-
// Flush all pending data to target
|
|
1147
|
-
if (connectionRecord.pendingData.length > 0) {
|
|
1148
|
-
const combinedData = Buffer.concat(connectionRecord.pendingData);
|
|
1149
|
-
targetSocket.write(combinedData, (err) => {
|
|
1150
|
-
if (err) {
|
|
1151
|
-
console.log(
|
|
1152
|
-
`[${connectionId}] Error writing pending data to target: ${err.message}`
|
|
1153
|
-
);
|
|
1154
|
-
return initiateCleanupOnce('write_error');
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// Now set up piping for future data and resume the socket
|
|
1158
|
-
socket.pipe(targetSocket);
|
|
1159
|
-
targetSocket.pipe(socket);
|
|
1160
|
-
socket.resume(); // Resume the socket after piping is established
|
|
1161
|
-
|
|
1162
|
-
if (this.settings.enableDetailedLogging) {
|
|
1163
|
-
console.log(
|
|
1164
|
-
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1165
|
-
`${
|
|
1166
|
-
serverName
|
|
1167
|
-
? ` (SNI: ${serverName})`
|
|
1168
|
-
: forcedDomain
|
|
1169
|
-
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1170
|
-
: ''
|
|
1171
|
-
}` +
|
|
1172
|
-
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Yes' : 'No'}`
|
|
1173
|
-
);
|
|
1174
|
-
} else {
|
|
1175
|
-
console.log(
|
|
1176
|
-
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1177
|
-
`${
|
|
1178
|
-
serverName
|
|
1179
|
-
? ` (SNI: ${serverName})`
|
|
1180
|
-
: forcedDomain
|
|
1181
|
-
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1182
|
-
: ''
|
|
1183
|
-
}`
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
});
|
|
1187
|
-
} else {
|
|
1188
|
-
// No pending data, so just set up piping
|
|
1189
|
-
socket.pipe(targetSocket);
|
|
1190
|
-
targetSocket.pipe(socket);
|
|
1191
|
-
socket.resume(); // Resume the socket after piping is established
|
|
1192
|
-
|
|
1193
|
-
if (this.settings.enableDetailedLogging) {
|
|
1194
|
-
console.log(
|
|
1195
|
-
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1196
|
-
`${
|
|
1197
|
-
serverName
|
|
1198
|
-
? ` (SNI: ${serverName})`
|
|
1199
|
-
: forcedDomain
|
|
1200
|
-
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1201
|
-
: ''
|
|
1202
|
-
}` +
|
|
1203
|
-
` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Yes' : 'No'}`
|
|
1204
|
-
);
|
|
1205
|
-
} else {
|
|
1206
|
-
console.log(
|
|
1207
|
-
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1208
|
-
`${
|
|
1209
|
-
serverName
|
|
1210
|
-
? ` (SNI: ${serverName})`
|
|
1211
|
-
: forcedDomain
|
|
1212
|
-
? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
|
|
1213
|
-
: ''
|
|
1214
|
-
}`
|
|
1215
|
-
);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
// Clear the buffer now that we've processed it
|
|
1220
|
-
connectionRecord.pendingData = [];
|
|
1221
|
-
connectionRecord.pendingDataSize = 0;
|
|
1222
|
-
|
|
1223
|
-
// Add the renegotiation listener for SNI validation
|
|
1224
|
-
if (serverName) {
|
|
1225
|
-
socket.on('data', (renegChunk: Buffer) => {
|
|
1226
|
-
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
1227
|
-
try {
|
|
1228
|
-
// Try to extract SNI from potential renegotiation
|
|
1229
|
-
const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1230
|
-
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
|
|
1231
|
-
console.log(
|
|
1232
|
-
`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`
|
|
1233
|
-
);
|
|
1234
|
-
initiateCleanupOnce('sni_mismatch');
|
|
1235
|
-
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
1236
|
-
console.log(
|
|
1237
|
-
`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
|
|
1238
|
-
);
|
|
1239
|
-
}
|
|
1240
|
-
} catch (err) {
|
|
1241
|
-
console.log(
|
|
1242
|
-
`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Set connection timeout with simpler logic
|
|
1250
|
-
if (connectionRecord.cleanupTimer) {
|
|
1251
|
-
clearTimeout(connectionRecord.cleanupTimer);
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
// For immortal keep-alive connections, skip setting a timeout completely
|
|
1255
|
-
if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
1256
|
-
if (this.settings.enableDetailedLogging) {
|
|
1257
|
-
console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
|
|
1258
|
-
}
|
|
1259
|
-
// No cleanup timer for immortal connections
|
|
1260
|
-
}
|
|
1261
|
-
// For extended keep-alive connections, use extended timeout
|
|
1262
|
-
else if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
1263
|
-
const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
1264
|
-
const safeTimeout = ensureSafeTimeout(extendedTimeout);
|
|
1265
|
-
|
|
1266
|
-
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
1267
|
-
console.log(
|
|
1268
|
-
`[${connectionId}] Keep-alive connection from ${remoteIP} exceeded extended lifetime (${plugins.prettyMs(
|
|
1269
|
-
extendedTimeout
|
|
1270
|
-
)}), forcing cleanup.`
|
|
1271
|
-
);
|
|
1272
|
-
initiateCleanupOnce('extended_lifetime');
|
|
1273
|
-
}, safeTimeout);
|
|
1274
|
-
|
|
1275
|
-
// Make sure timeout doesn't keep the process alive
|
|
1276
|
-
if (connectionRecord.cleanupTimer.unref) {
|
|
1277
|
-
connectionRecord.cleanupTimer.unref();
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (this.settings.enableDetailedLogging) {
|
|
1281
|
-
console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
// For standard connections, use normal timeout
|
|
1285
|
-
else {
|
|
1286
|
-
// Use domain-specific timeout if available, otherwise use default
|
|
1287
|
-
const connectionTimeout = connectionRecord.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
|
|
1288
|
-
const safeTimeout = ensureSafeTimeout(connectionTimeout);
|
|
1289
|
-
|
|
1290
|
-
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
1291
|
-
console.log(
|
|
1292
|
-
`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(
|
|
1293
|
-
connectionTimeout
|
|
1294
|
-
)}), forcing cleanup.`
|
|
1295
|
-
);
|
|
1296
|
-
initiateCleanupOnce('connection_timeout');
|
|
1297
|
-
}, safeTimeout);
|
|
1298
|
-
|
|
1299
|
-
// Make sure timeout doesn't keep the process alive
|
|
1300
|
-
if (connectionRecord.cleanupTimer.unref) {
|
|
1301
|
-
connectionRecord.cleanupTimer.unref();
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
// Mark TLS handshake as complete for TLS connections
|
|
1306
|
-
if (connectionRecord.isTLS) {
|
|
1307
|
-
connectionRecord.tlsHandshakeComplete = true;
|
|
1308
|
-
|
|
1309
|
-
if (this.settings.enableTlsDebugLogging) {
|
|
1310
|
-
console.log(
|
|
1311
|
-
`[${connectionId}] TLS handshake complete for connection from ${remoteIP}`
|
|
1312
|
-
);
|
|
1313
|
-
}
|
|
1314
|
-
}
|
|
1315
|
-
});
|
|
1453
|
+
// If we didn't forward to NetworkProxy, proceed with direct connection
|
|
1454
|
+
return this.setupDirectConnection(
|
|
1455
|
+
connectionId,
|
|
1456
|
+
socket,
|
|
1457
|
+
connectionRecord,
|
|
1458
|
+
domainConfig,
|
|
1459
|
+
serverName,
|
|
1460
|
+
initialChunk,
|
|
1461
|
+
overridePort
|
|
1462
|
+
);
|
|
1316
1463
|
};
|
|
1317
1464
|
|
|
1318
1465
|
// --- PORT RANGE-BASED HANDLING ---
|
|
@@ -1475,7 +1622,7 @@ export class PortProxy {
|
|
|
1475
1622
|
console.log(
|
|
1476
1623
|
`PortProxy -> OK: Now listening on port ${port}${
|
|
1477
1624
|
this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
|
|
1478
|
-
}`
|
|
1625
|
+
}${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}`
|
|
1479
1626
|
);
|
|
1480
1627
|
});
|
|
1481
1628
|
this.netServers.push(server);
|
|
@@ -1494,6 +1641,7 @@ export class PortProxy {
|
|
|
1494
1641
|
let completedTlsHandshakes = 0;
|
|
1495
1642
|
let pendingTlsHandshakes = 0;
|
|
1496
1643
|
let keepAliveConnections = 0;
|
|
1644
|
+
let networkProxyConnections = 0;
|
|
1497
1645
|
|
|
1498
1646
|
// Create a copy of the keys to avoid modification during iteration
|
|
1499
1647
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
@@ -1517,6 +1665,10 @@ export class PortProxy {
|
|
|
1517
1665
|
if (record.hasKeepAlive) {
|
|
1518
1666
|
keepAliveConnections++;
|
|
1519
1667
|
}
|
|
1668
|
+
|
|
1669
|
+
if (record.usingNetworkProxy) {
|
|
1670
|
+
networkProxyConnections++;
|
|
1671
|
+
}
|
|
1520
1672
|
|
|
1521
1673
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
|
1522
1674
|
if (record.outgoingStartTime) {
|
|
@@ -1613,7 +1765,7 @@ export class PortProxy {
|
|
|
1613
1765
|
console.log(
|
|
1614
1766
|
`Active connections: ${this.connectionRecords.size}. ` +
|
|
1615
1767
|
`Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
|
|
1616
|
-
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}. ` +
|
|
1768
|
+
`Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
|
|
1617
1769
|
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
|
|
1618
1770
|
maxOutgoing
|
|
1619
1771
|
)}. ` +
|
|
@@ -1630,6 +1782,21 @@ export class PortProxy {
|
|
|
1630
1782
|
}
|
|
1631
1783
|
}
|
|
1632
1784
|
|
|
1785
|
+
/**
|
|
1786
|
+
* Add or replace NetworkProxy instances
|
|
1787
|
+
*/
|
|
1788
|
+
public setNetworkProxies(networkProxies: NetworkProxy[]): void {
|
|
1789
|
+
this.networkProxies = networkProxies;
|
|
1790
|
+
console.log(`Updated NetworkProxy instances: ${this.networkProxies.length} proxies configured`);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
/**
|
|
1794
|
+
* Get a list of configured NetworkProxy instances
|
|
1795
|
+
*/
|
|
1796
|
+
public getNetworkProxies(): NetworkProxy[] {
|
|
1797
|
+
return this.networkProxies;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1633
1800
|
/**
|
|
1634
1801
|
* Gracefully shut down the proxy
|
|
1635
1802
|
*/
|