@push.rocks/smartproxy 3.28.6 → 3.29.1

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.
@@ -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
- // Helper to reject an incoming connection
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', (err: 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
- if (initialChunk && isTlsHandshake(initialChunk)) {
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
- const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
930
- const connectionOptions: plugins.net.NetConnectOpts = {
931
- host: targetHost,
932
- port: overridePort !== undefined ? overridePort : this.settings.toPort,
933
- };
934
- if (this.settings.preserveSourceIP) {
935
- connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
936
- }
937
-
938
- // Pause the incoming socket to prevent buffer overflows
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
  */