@push.rocks/smartproxy 3.32.2 → 3.34.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.
@@ -10,10 +10,6 @@ export interface IDomainConfig {
10
10
  portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
11
11
  // Allow domain-specific timeout override
12
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)
17
13
  }
18
14
 
19
15
  /** Port proxy settings including global allowed port ranges */
@@ -54,14 +50,15 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
54
50
  // Rate limiting and security
55
51
  maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
56
52
  connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
57
-
53
+
58
54
  // Enhanced keep-alive settings
59
55
  keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
60
56
  keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
61
57
  extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
62
-
58
+
63
59
  // New property for NetworkProxy integration
64
- networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
60
+ useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
61
+ networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
65
62
  }
66
63
 
67
64
  /**
@@ -90,16 +87,22 @@ interface IConnectionRecord {
90
87
  tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
91
88
  hasReceivedInitialData: boolean; // Whether initial data has been received
92
89
  domainConfig?: IDomainConfig; // Associated domain config for this connection
93
-
90
+
94
91
  // Keep-alive tracking
95
92
  hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
96
93
  inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
97
94
  incomingTerminationReason?: string | null; // Reason for incoming termination
98
95
  outgoingTerminationReason?: string | null; // Reason for outgoing termination
99
-
100
- // New field for NetworkProxy tracking
96
+
97
+ // NetworkProxy tracking
101
98
  usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
102
- networkProxyIndex?: number; // Which NetworkProxy instance is being used
99
+
100
+ // Renegotiation handler
101
+ renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
102
+
103
+ // Browser connection tracking
104
+ isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
105
+ domainSwitches?: number; // Number of times the domain has been switched on this connection
103
106
  }
104
107
 
105
108
  /**
@@ -266,6 +269,29 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
266
269
  }
267
270
  }
268
271
 
272
+ /**
273
+ * Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type)
274
+ * @param buffer - Buffer containing the TLS record
275
+ * @returns true if the buffer contains a proper ClientHello message
276
+ */
277
+ function isClientHello(buffer: Buffer): boolean {
278
+ try {
279
+ if (buffer.length < 9) return false; // Too small for a proper ClientHello
280
+
281
+ // Check record type (has to be handshake - 22)
282
+ if (buffer.readUInt8(0) !== 22) return false;
283
+
284
+ // After the TLS record header (5 bytes), check the handshake type (1 for ClientHello)
285
+ if (buffer.readUInt8(5) !== 1) return false;
286
+
287
+ // Basic checks passed, this appears to be a ClientHello
288
+ return true;
289
+ } catch (err) {
290
+ console.log(`Error checking for ClientHello: ${err}`);
291
+ return false;
292
+ }
293
+ }
294
+
269
295
  // Helper: Check if a port falls within any of the given port ranges
270
296
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
271
297
  return ranges.some((range) => port >= range.from && port <= range.to);
@@ -348,9 +374,9 @@ export class PortProxy {
348
374
  // Connection tracking by IP for rate limiting
349
375
  private connectionsByIP: Map<string, Set<string>> = new Map();
350
376
  private connectionRateByIP: Map<string, number[]> = new Map();
351
-
352
- // New property to store NetworkProxy instances
353
- private networkProxies: NetworkProxy[] = [];
377
+
378
+ // NetworkProxy instance for TLS termination
379
+ private networkProxy: NetworkProxy | null = null;
354
380
 
355
381
  constructor(settingsArg: IPortProxySettings) {
356
382
  // Set reasonable defaults for all settings
@@ -370,29 +396,49 @@ export class PortProxy {
370
396
  // Socket optimization settings
371
397
  noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
372
398
  keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
373
- keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
374
- maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
399
+ keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
400
+ maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB
375
401
 
376
402
  // Feature flags
377
403
  disableInactivityCheck: settingsArg.disableInactivityCheck || false,
378
404
  enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
379
- ? settingsArg.enableKeepAliveProbes : true, // Enable by default
405
+ ? settingsArg.enableKeepAliveProbes : true,
380
406
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
381
407
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
382
- enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
408
+ enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
383
409
 
384
410
  // Rate limiting defaults
385
- maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
386
- connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
387
-
411
+ maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
412
+ connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
413
+
388
414
  // Enhanced keep-alive settings
389
- keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
390
- keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
415
+ keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
416
+ keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
391
417
  extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
418
+
419
+ // NetworkProxy settings
420
+ networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
392
421
  };
393
-
394
- // Store NetworkProxy instances if provided
395
- this.networkProxies = settingsArg.networkProxies || [];
422
+
423
+ // Initialize NetworkProxy if enabled
424
+ if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
425
+ this.initializeNetworkProxy();
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Initialize NetworkProxy instance
431
+ */
432
+ private initializeNetworkProxy(): void {
433
+ if (!this.networkProxy) {
434
+ this.networkProxy = new NetworkProxy({
435
+ port: this.settings.networkProxyPort!,
436
+ portProxyIntegration: true,
437
+ logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
438
+ });
439
+
440
+ console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
441
+ }
396
442
  }
397
443
 
398
444
  /**
@@ -400,71 +446,69 @@ export class PortProxy {
400
446
  * @param connectionId - Unique connection identifier
401
447
  * @param socket - The incoming client socket
402
448
  * @param record - The connection record
403
- * @param domainConfig - The domain configuration
404
449
  * @param initialData - Initial data chunk (TLS ClientHello)
405
- * @param serverName - SNI hostname (if available)
406
450
  */
407
451
  private forwardToNetworkProxy(
408
452
  connectionId: string,
409
453
  socket: plugins.net.Socket,
410
454
  record: IConnectionRecord,
411
- domainConfig: IDomainConfig,
412
- initialData: Buffer,
413
- serverName?: string
455
+ initialData: Buffer
414
456
  ): 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.`);
457
+ // Ensure NetworkProxy is initialized
458
+ if (!this.networkProxy) {
459
+ console.log(
460
+ `[${connectionId}] NetworkProxy not initialized. Using fallback direct connection.`
461
+ );
423
462
  // Fall back to direct connection
424
- return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData);
463
+ return this.setupDirectConnection(
464
+ connectionId,
465
+ socket,
466
+ record,
467
+ undefined,
468
+ undefined,
469
+ initialData
470
+ );
425
471
  }
426
-
427
- const networkProxy = this.networkProxies[proxyIndex];
428
- const proxyPort = networkProxy.getListeningPort();
472
+
473
+ const proxyPort = this.networkProxy.getListeningPort();
429
474
  const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
430
-
475
+
431
476
  if (this.settings.enableDetailedLogging) {
432
477
  console.log(
433
- `[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
478
+ `[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
434
479
  );
435
480
  }
436
-
481
+
437
482
  // Create a connection to the NetworkProxy
438
483
  const proxySocket = plugins.net.connect({
439
484
  host: proxyHost,
440
- port: proxyPort
485
+ port: proxyPort,
441
486
  });
442
-
487
+
443
488
  // Store the outgoing socket in the record
444
489
  record.outgoing = proxySocket;
445
490
  record.outgoingStartTime = Date.now();
446
491
  record.usingNetworkProxy = true;
447
- record.networkProxyIndex = proxyIndex;
448
-
492
+
449
493
  // Set up error handlers
450
494
  proxySocket.on('error', (err) => {
451
495
  console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
452
496
  this.cleanupConnection(record, 'network_proxy_connect_error');
453
497
  });
454
-
498
+
455
499
  // Handle connection to NetworkProxy
456
500
  proxySocket.on('connect', () => {
457
501
  if (this.settings.enableDetailedLogging) {
458
502
  console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
459
503
  }
460
-
504
+
461
505
  // First send the initial data that contains the TLS ClientHello
462
506
  proxySocket.write(initialData);
463
-
507
+
464
508
  // Now set up bidirectional piping between client and NetworkProxy
465
509
  socket.pipe(proxySocket);
466
510
  proxySocket.pipe(socket);
467
-
511
+
468
512
  // Setup cleanup handlers
469
513
  proxySocket.on('close', () => {
470
514
  if (this.settings.enableDetailedLogging) {
@@ -472,26 +516,28 @@ export class PortProxy {
472
516
  }
473
517
  this.cleanupConnection(record, 'network_proxy_closed');
474
518
  });
475
-
519
+
476
520
  socket.on('close', () => {
477
521
  if (this.settings.enableDetailedLogging) {
478
- console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`);
522
+ console.log(
523
+ `[${connectionId}] Client connection closed after forwarding to NetworkProxy`
524
+ );
479
525
  }
480
526
  this.cleanupConnection(record, 'client_closed');
481
527
  });
482
-
528
+
483
529
  // Update activity on data transfer
484
530
  socket.on('data', () => this.updateActivity(record));
485
531
  proxySocket.on('data', () => this.updateActivity(record));
486
-
532
+
487
533
  if (this.settings.enableDetailedLogging) {
488
534
  console.log(
489
- `[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
535
+ `[${connectionId}] TLS connection successfully forwarded to NetworkProxy`
490
536
  );
491
537
  }
492
538
  });
493
539
  }
494
-
540
+
495
541
  /**
496
542
  * Sets up a direct connection to the target (original behavior)
497
543
  * This is used when NetworkProxy isn't configured or as a fallback
@@ -568,11 +614,11 @@ export class PortProxy {
568
614
 
569
615
  // Apply socket optimizations
570
616
  targetSocket.setNoDelay(this.settings.noDelay);
571
-
617
+
572
618
  // Apply keep-alive settings to the outgoing connection as well
573
619
  if (this.settings.keepAlive) {
574
620
  targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
575
-
621
+
576
622
  // Apply enhanced TCP keep-alive options if enabled
577
623
  if (this.settings.enableKeepAliveProbes) {
578
624
  try {
@@ -585,7 +631,9 @@ export class PortProxy {
585
631
  } catch (err) {
586
632
  // Ignore errors - these are optional enhancements
587
633
  if (this.settings.enableDetailedLogging) {
588
- console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
634
+ console.log(
635
+ `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
636
+ );
589
637
  }
590
638
  }
591
639
  }
@@ -642,19 +690,21 @@ export class PortProxy {
642
690
  // For keep-alive connections, just log a warning instead of closing
643
691
  if (record.hasKeepAlive) {
644
692
  console.log(
645
- `[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
693
+ `[${connectionId}] Timeout event on incoming keep-alive connection from ${
694
+ record.remoteIP
695
+ } after ${plugins.prettyMs(
646
696
  this.settings.socketTimeout || 3600000
647
697
  )}. Connection preserved.`
648
698
  );
649
699
  // Don't close the connection - just log
650
700
  return;
651
701
  }
652
-
702
+
653
703
  // For non-keep-alive connections, proceed with normal cleanup
654
704
  console.log(
655
- `[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(
656
- this.settings.socketTimeout || 3600000
657
- )}`
705
+ `[${connectionId}] Timeout on incoming side from ${
706
+ record.remoteIP
707
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
658
708
  );
659
709
  if (record.incomingTerminationReason === null) {
660
710
  record.incomingTerminationReason = 'timeout';
@@ -667,19 +717,21 @@ export class PortProxy {
667
717
  // For keep-alive connections, just log a warning instead of closing
668
718
  if (record.hasKeepAlive) {
669
719
  console.log(
670
- `[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
720
+ `[${connectionId}] Timeout event on outgoing keep-alive connection from ${
721
+ record.remoteIP
722
+ } after ${plugins.prettyMs(
671
723
  this.settings.socketTimeout || 3600000
672
724
  )}. Connection preserved.`
673
725
  );
674
726
  // Don't close the connection - just log
675
727
  return;
676
728
  }
677
-
729
+
678
730
  // For non-keep-alive connections, proceed with normal cleanup
679
731
  console.log(
680
- `[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(
681
- this.settings.socketTimeout || 3600000
682
- )}`
732
+ `[${connectionId}] Timeout on outgoing side from ${
733
+ record.remoteIP
734
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
683
735
  );
684
736
  if (record.outgoingTerminationReason === null) {
685
737
  record.outgoingTerminationReason = 'timeout';
@@ -693,9 +745,11 @@ export class PortProxy {
693
745
  // Disable timeouts completely for immortal connections
694
746
  socket.setTimeout(0);
695
747
  targetSocket.setTimeout(0);
696
-
748
+
697
749
  if (this.settings.enableDetailedLogging) {
698
- console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
750
+ console.log(
751
+ `[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`
752
+ );
699
753
  }
700
754
  } else {
701
755
  // Set normal timeouts for other connections
@@ -725,9 +779,7 @@ export class PortProxy {
725
779
  const combinedData = Buffer.concat(record.pendingData);
726
780
  targetSocket.write(combinedData, (err) => {
727
781
  if (err) {
728
- console.log(
729
- `[${connectionId}] Error writing pending data to target: ${err.message}`
730
- );
782
+ console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
731
783
  return this.initiateCleanupOnce(record, 'write_error');
732
784
  }
733
785
 
@@ -746,7 +798,9 @@ export class PortProxy {
746
798
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
747
799
  : ''
748
800
  }` +
749
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
801
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
802
+ record.hasKeepAlive ? 'Yes' : 'No'
803
+ }`
750
804
  );
751
805
  } else {
752
806
  console.log(
@@ -777,7 +831,9 @@ export class PortProxy {
777
831
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
778
832
  : ''
779
833
  }` +
780
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
834
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
835
+ record.hasKeepAlive ? 'Yes' : 'No'
836
+ }`
781
837
  );
782
838
  } else {
783
839
  console.log(
@@ -797,82 +853,104 @@ export class PortProxy {
797
853
  record.pendingData = [];
798
854
  record.pendingDataSize = 0;
799
855
 
800
- // Add the renegotiation listener for SNI validation
856
+ // Add the renegotiation handler for SNI validation with strict domain enforcement
801
857
  if (serverName) {
802
- socket.on('data', (renegChunk: Buffer) => {
803
- if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
858
+ // Define a handler for checking renegotiation with improved detection
859
+ const renegotiationHandler = (renegChunk: Buffer) => {
860
+ // Only process if this looks like a TLS ClientHello
861
+ if (isClientHello(renegChunk)) {
804
862
  try {
805
- // Try to extract SNI from potential renegotiation
863
+ // Extract SNI from ClientHello
806
864
  const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
807
- if (newSNI && newSNI !== record.lockedDomain) {
865
+
866
+ // Skip if no SNI was found
867
+ if (!newSNI) return;
868
+
869
+ // Handle SNI change during renegotiation - always terminate for domain switches
870
+ if (newSNI !== record.lockedDomain) {
871
+ // Log and terminate the connection for any SNI change
808
872
  console.log(
809
- `[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
873
+ `[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
874
+ `Terminating connection - SNI domain switching is not allowed.`
810
875
  );
811
876
  this.initiateCleanupOnce(record, 'sni_mismatch');
812
- } else if (newSNI && this.settings.enableDetailedLogging) {
877
+ } else if (this.settings.enableDetailedLogging) {
813
878
  console.log(
814
- `[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
879
+ `[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
815
880
  );
816
881
  }
817
882
  } catch (err) {
818
883
  console.log(
819
- `[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
884
+ `[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
820
885
  );
821
886
  }
822
887
  }
823
- });
888
+ };
889
+
890
+ // Store the handler in the connection record so we can remove it during cleanup
891
+ record.renegotiationHandler = renegotiationHandler;
892
+
893
+ // Add the listener
894
+ socket.on('data', renegotiationHandler);
824
895
  }
825
896
 
826
897
  // Set connection timeout with simpler logic
827
898
  if (record.cleanupTimer) {
828
899
  clearTimeout(record.cleanupTimer);
829
900
  }
830
-
901
+
831
902
  // For immortal keep-alive connections, skip setting a timeout completely
832
903
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
833
904
  if (this.settings.enableDetailedLogging) {
834
- console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
905
+ console.log(
906
+ `[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
907
+ );
835
908
  }
836
909
  // No cleanup timer for immortal connections
837
- }
910
+ }
838
911
  // For extended keep-alive connections, use extended timeout
839
912
  else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
840
913
  const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
841
914
  const safeTimeout = ensureSafeTimeout(extendedTimeout);
842
-
915
+
843
916
  record.cleanupTimer = setTimeout(() => {
844
917
  console.log(
845
- `[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs(
846
- extendedTimeout
847
- )}), forcing cleanup.`
918
+ `[${connectionId}] Keep-alive connection from ${
919
+ record.remoteIP
920
+ } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
848
921
  );
849
922
  this.initiateCleanupOnce(record, 'extended_lifetime');
850
923
  }, safeTimeout);
851
-
924
+
852
925
  // Make sure timeout doesn't keep the process alive
853
926
  if (record.cleanupTimer.unref) {
854
927
  record.cleanupTimer.unref();
855
928
  }
856
-
929
+
857
930
  if (this.settings.enableDetailedLogging) {
858
- console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
931
+ console.log(
932
+ `[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
933
+ extendedTimeout
934
+ )}`
935
+ );
859
936
  }
860
937
  }
861
938
  // For standard connections, use normal timeout
862
939
  else {
863
940
  // Use domain-specific timeout if available, otherwise use default
864
- const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
941
+ const connectionTimeout =
942
+ record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
865
943
  const safeTimeout = ensureSafeTimeout(connectionTimeout);
866
-
944
+
867
945
  record.cleanupTimer = setTimeout(() => {
868
946
  console.log(
869
- `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs(
870
- connectionTimeout
871
- )}), forcing cleanup.`
947
+ `[${connectionId}] Connection from ${
948
+ record.remoteIP
949
+ } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
872
950
  );
873
951
  this.initiateCleanupOnce(record, 'connection_timeout');
874
952
  }, safeTimeout);
875
-
953
+
876
954
  // Make sure timeout doesn't keep the process alive
877
955
  if (record.cleanupTimer.unref) {
878
956
  record.cleanupTimer.unref();
@@ -973,6 +1051,16 @@ export class PortProxy {
973
1051
  const bytesReceived = record.bytesReceived;
974
1052
  const bytesSent = record.bytesSent;
975
1053
 
1054
+ // Remove the renegotiation handler if present
1055
+ if (record.renegotiationHandler && record.incoming) {
1056
+ try {
1057
+ record.incoming.removeListener('data', record.renegotiationHandler);
1058
+ record.renegotiationHandler = undefined;
1059
+ } catch (err) {
1060
+ console.log(`[${record.id}] Error removing renegotiation handler: ${err}`);
1061
+ }
1062
+ }
1063
+
976
1064
  try {
977
1065
  if (!record.incoming.destroyed) {
978
1066
  // Try graceful shutdown first, then force destroy after a short timeout
@@ -1047,8 +1135,11 @@ export class PortProxy {
1047
1135
  ` Duration: ${plugins.prettyMs(
1048
1136
  duration
1049
1137
  )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
1050
- `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
1051
- `${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
1138
+ `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1139
+ record.hasKeepAlive ? 'Yes' : 'No'
1140
+ }` +
1141
+ `${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
1142
+ `${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
1052
1143
  );
1053
1144
  } else {
1054
1145
  console.log(
@@ -1063,7 +1154,7 @@ export class PortProxy {
1063
1154
  */
1064
1155
  private updateActivity(record: IConnectionRecord): void {
1065
1156
  record.lastActivity = Date.now();
1066
-
1157
+
1067
1158
  // Clear any inactivity warning
1068
1159
  if (record.inactivityWarningIssued) {
1069
1160
  record.inactivityWarningIssued = false;
@@ -1082,7 +1173,7 @@ export class PortProxy {
1082
1173
  }
1083
1174
  return this.settings.targetIP!;
1084
1175
  }
1085
-
1176
+
1086
1177
  /**
1087
1178
  * Initiates cleanup once for a connection
1088
1179
  */
@@ -1090,12 +1181,15 @@ export class PortProxy {
1090
1181
  if (this.settings.enableDetailedLogging) {
1091
1182
  console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
1092
1183
  }
1093
-
1094
- if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) {
1184
+
1185
+ if (
1186
+ record.incomingTerminationReason === null ||
1187
+ record.incomingTerminationReason === undefined
1188
+ ) {
1095
1189
  record.incomingTerminationReason = reason;
1096
1190
  this.incrementTerminationStat('incoming', reason);
1097
1191
  }
1098
-
1192
+
1099
1193
  this.cleanupConnection(record, reason);
1100
1194
  }
1101
1195
 
@@ -1184,6 +1278,12 @@ export class PortProxy {
1184
1278
  return;
1185
1279
  }
1186
1280
 
1281
+ // Start NetworkProxy if configured
1282
+ if (this.networkProxy) {
1283
+ await this.networkProxy.start();
1284
+ console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
1285
+ }
1286
+
1187
1287
  // Define a unified connection handler for all listening ports.
1188
1288
  const connectionHandler = (socket: plugins.net.Socket) => {
1189
1289
  if (this.isShuttingDown) {
@@ -1219,7 +1319,7 @@ export class PortProxy {
1219
1319
 
1220
1320
  // Apply socket optimizations
1221
1321
  socket.setNoDelay(this.settings.noDelay);
1222
-
1322
+
1223
1323
  // Create a unique connection ID and record
1224
1324
  const connectionId = generateConnectionId();
1225
1325
  const connectionRecord: IConnectionRecord = {
@@ -1243,16 +1343,20 @@ export class PortProxy {
1243
1343
  hasKeepAlive: false, // Will set to true if keep-alive is applied
1244
1344
  incomingTerminationReason: null,
1245
1345
  outgoingTerminationReason: null,
1246
-
1247
- // Initialize NetworkProxy tracking fields
1248
- usingNetworkProxy: false
1346
+
1347
+ // Initialize NetworkProxy tracking
1348
+ usingNetworkProxy: false,
1349
+
1350
+ // Initialize browser connection tracking
1351
+ isBrowserConnection: false,
1352
+ domainSwitches: 0,
1249
1353
  };
1250
-
1354
+
1251
1355
  // Apply keep-alive settings if enabled
1252
1356
  if (this.settings.keepAlive) {
1253
1357
  socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
1254
1358
  connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
1255
-
1359
+
1256
1360
  // Apply enhanced TCP keep-alive options if enabled
1257
1361
  if (this.settings.enableKeepAliveProbes) {
1258
1362
  try {
@@ -1266,7 +1370,9 @@ export class PortProxy {
1266
1370
  } catch (err) {
1267
1371
  // Ignore errors - these are optional enhancements
1268
1372
  if (this.settings.enableDetailedLogging) {
1269
- console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
1373
+ console.log(
1374
+ `[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
1375
+ );
1270
1376
  }
1271
1377
  }
1272
1378
  }
@@ -1279,8 +1385,8 @@ export class PortProxy {
1279
1385
  if (this.settings.enableDetailedLogging) {
1280
1386
  console.log(
1281
1387
  `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
1282
- `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
1283
- `Active connections: ${this.connectionRecords.size}`
1388
+ `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
1389
+ `Active connections: ${this.connectionRecords.size}`
1284
1390
  );
1285
1391
  } else {
1286
1392
  console.log(
@@ -1288,23 +1394,16 @@ export class PortProxy {
1288
1394
  );
1289
1395
  }
1290
1396
 
1291
- let initialDataReceived = false;
1397
+ // Check if this connection should be forwarded directly to NetworkProxy based on port
1398
+ const shouldUseNetworkProxy = this.settings.useNetworkProxy &&
1399
+ this.settings.useNetworkProxy.includes(localPort);
1292
1400
 
1293
- // Define helpers for rejecting connections
1294
- const rejectIncomingConnection = (reason: string, logMessage: string) => {
1295
- console.log(`[${connectionId}] ${logMessage}`);
1296
- socket.end();
1297
- if (connectionRecord.incomingTerminationReason === null) {
1298
- connectionRecord.incomingTerminationReason = reason;
1299
- this.incrementTerminationStat('incoming', reason);
1300
- }
1301
- this.cleanupConnection(connectionRecord, reason);
1302
- };
1401
+ if (shouldUseNetworkProxy) {
1402
+ // For NetworkProxy ports, we want to capture the TLS handshake and forward directly
1403
+ let initialDataReceived = false;
1303
1404
 
1304
- // Set an initial timeout for SNI data if needed
1305
- let initialTimeout: NodeJS.Timeout | null = null;
1306
- if (this.settings.sniEnabled) {
1307
- initialTimeout = setTimeout(() => {
1405
+ // Set an initial timeout for handshake data
1406
+ let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
1308
1407
  if (!initialDataReceived) {
1309
1408
  console.log(
1310
1409
  `[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
@@ -1322,278 +1421,331 @@ export class PortProxy {
1322
1421
  if (initialTimeout.unref) {
1323
1422
  initialTimeout.unref();
1324
1423
  }
1325
- } else {
1326
- initialDataReceived = true;
1327
- connectionRecord.hasReceivedInitialData = true;
1328
- }
1329
1424
 
1330
- socket.on('error', this.handleError('incoming', connectionRecord));
1425
+ socket.on('error', this.handleError('incoming', connectionRecord));
1331
1426
 
1332
- // Track data for bytes counting
1333
- socket.on('data', (chunk: Buffer) => {
1334
- connectionRecord.bytesReceived += chunk.length;
1335
- this.updateActivity(connectionRecord);
1427
+ // First data handler to capture initial TLS handshake for NetworkProxy
1428
+ socket.once('data', (chunk: Buffer) => {
1429
+ // Clear the initial timeout since we've received data
1430
+ if (initialTimeout) {
1431
+ clearTimeout(initialTimeout);
1432
+ initialTimeout = null;
1433
+ }
1336
1434
 
1337
- // Check for TLS handshake if this is the first chunk
1338
- if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
1339
- connectionRecord.isTLS = true;
1435
+ initialDataReceived = true;
1436
+ connectionRecord.hasReceivedInitialData = true;
1340
1437
 
1341
- if (this.settings.enableTlsDebugLogging) {
1342
- console.log(
1343
- `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
1344
- );
1345
- // Try to extract SNI and log detailed debug info
1346
- extractSNI(chunk, true);
1438
+ // Check if this looks like a TLS handshake
1439
+ if (isTlsHandshake(chunk)) {
1440
+ connectionRecord.isTLS = true;
1441
+
1442
+ // Forward directly to NetworkProxy without SNI processing
1443
+ this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
1444
+ } else {
1445
+ // If not TLS, use normal direct connection
1446
+ console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
1447
+ this.setupDirectConnection(connectionId, socket, connectionRecord, undefined, undefined, chunk);
1347
1448
  }
1348
- }
1349
- });
1350
-
1351
- /**
1352
- * Sets up the connection to the target host or NetworkProxy.
1353
- * @param serverName - The SNI hostname (unused when forcedDomain is provided).
1354
- * @param initialChunk - Optional initial data chunk.
1355
- * @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
1356
- * @param overridePort - If provided, use this port for the outgoing connection.
1357
- */
1358
- const setupConnection = (
1359
- serverName: string,
1360
- initialChunk?: Buffer,
1361
- forcedDomain?: IDomainConfig,
1362
- overridePort?: number
1363
- ) => {
1364
- // Clear the initial timeout since we've received data
1365
- if (initialTimeout) {
1366
- clearTimeout(initialTimeout);
1367
- initialTimeout = null;
1368
- }
1449
+ });
1450
+
1451
+ } else {
1452
+ // For non-NetworkProxy ports, proceed with normal processing
1453
+
1454
+ // Define helpers for rejecting connections
1455
+ const rejectIncomingConnection = (reason: string, logMessage: string) => {
1456
+ console.log(`[${connectionId}] ${logMessage}`);
1457
+ socket.end();
1458
+ if (connectionRecord.incomingTerminationReason === null) {
1459
+ connectionRecord.incomingTerminationReason = reason;
1460
+ this.incrementTerminationStat('incoming', reason);
1461
+ }
1462
+ this.cleanupConnection(connectionRecord, reason);
1463
+ };
1369
1464
 
1370
- // Mark that we've received initial data
1371
- initialDataReceived = true;
1372
- connectionRecord.hasReceivedInitialData = true;
1465
+ let initialDataReceived = false;
1373
1466
 
1374
- // Check if this looks like a TLS handshake
1375
- const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
1376
- if (isTlsHandshakeDetected) {
1377
- connectionRecord.isTLS = true;
1467
+ // Set an initial timeout for SNI data if needed
1468
+ let initialTimeout: NodeJS.Timeout | null = null;
1469
+ if (this.settings.sniEnabled) {
1470
+ initialTimeout = setTimeout(() => {
1471
+ if (!initialDataReceived) {
1472
+ console.log(
1473
+ `[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
1474
+ );
1475
+ if (connectionRecord.incomingTerminationReason === null) {
1476
+ connectionRecord.incomingTerminationReason = 'initial_timeout';
1477
+ this.incrementTerminationStat('incoming', 'initial_timeout');
1478
+ }
1479
+ socket.end();
1480
+ this.cleanupConnection(connectionRecord, 'initial_timeout');
1481
+ }
1482
+ }, this.settings.initialDataTimeout!);
1378
1483
 
1379
- if (this.settings.enableTlsDebugLogging) {
1380
- console.log(
1381
- `[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
1382
- );
1484
+ // Make sure timeout doesn't keep the process alive
1485
+ if (initialTimeout.unref) {
1486
+ initialTimeout.unref();
1383
1487
  }
1488
+ } else {
1489
+ initialDataReceived = true;
1490
+ connectionRecord.hasReceivedInitialData = true;
1384
1491
  }
1385
1492
 
1386
- // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
1387
- const domainConfig = forcedDomain
1388
- ? forcedDomain
1389
- : serverName
1390
- ? this.settings.domainConfigs.find((config) =>
1391
- config.domains.some((d) => plugins.minimatch(serverName, d))
1392
- )
1393
- : undefined;
1394
-
1395
- // Save domain config in connection record
1396
- connectionRecord.domainConfig = domainConfig;
1397
-
1398
- // IP validation is skipped if allowedIPs is empty
1399
- if (domainConfig) {
1400
- const effectiveAllowedIPs: string[] = [
1401
- ...domainConfig.allowedIPs,
1402
- ...(this.settings.defaultAllowedIPs || []),
1403
- ];
1404
- const effectiveBlockedIPs: string[] = [
1405
- ...(domainConfig.blockedIPs || []),
1406
- ...(this.settings.defaultBlockedIPs || []),
1407
- ];
1408
-
1409
- // Skip IP validation if allowedIPs is empty
1410
- if (
1411
- domainConfig.allowedIPs.length > 0 &&
1412
- !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)
1413
- ) {
1414
- return rejectIncomingConnection(
1415
- 'rejected',
1416
- `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(
1417
- ', '
1418
- )}`
1419
- );
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
- );
1493
+ socket.on('error', this.handleError('incoming', connectionRecord));
1494
+
1495
+ // Track data for bytes counting
1496
+ socket.on('data', (chunk: Buffer) => {
1497
+ connectionRecord.bytesReceived += chunk.length;
1498
+ this.updateActivity(connectionRecord);
1499
+
1500
+ // Check for TLS handshake if this is the first chunk
1501
+ if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
1502
+ connectionRecord.isTLS = true;
1503
+
1504
+ if (this.settings.enableTlsDebugLogging) {
1505
+ console.log(
1506
+ `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
1507
+ );
1508
+ // Try to extract SNI and log detailed debug info
1509
+ extractSNI(chunk, true);
1510
+ }
1437
1511
  }
1438
- } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
1439
- if (
1440
- !isGlobIPAllowed(
1441
- remoteIP,
1442
- this.settings.defaultAllowedIPs,
1443
- this.settings.defaultBlockedIPs || []
1444
- )
1445
- ) {
1446
- return rejectIncomingConnection(
1447
- 'rejected',
1448
- `Connection rejected: IP ${remoteIP} not allowed by default allowed list`
1449
- );
1512
+ });
1513
+
1514
+ /**
1515
+ * Sets up the connection to the target host.
1516
+ * @param serverName - The SNI hostname (unused when forcedDomain is provided).
1517
+ * @param initialChunk - Optional initial data chunk.
1518
+ * @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
1519
+ * @param overridePort - If provided, use this port for the outgoing connection.
1520
+ */
1521
+ const setupConnection = (
1522
+ serverName: string,
1523
+ initialChunk?: Buffer,
1524
+ forcedDomain?: IDomainConfig,
1525
+ overridePort?: number
1526
+ ) => {
1527
+ // Clear the initial timeout since we've received data
1528
+ if (initialTimeout) {
1529
+ clearTimeout(initialTimeout);
1530
+ initialTimeout = null;
1450
1531
  }
1451
- }
1452
1532
 
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
- );
1463
- };
1533
+ // Mark that we've received initial data
1534
+ initialDataReceived = true;
1535
+ connectionRecord.hasReceivedInitialData = true;
1464
1536
 
1465
- // --- PORT RANGE-BASED HANDLING ---
1466
- // Only apply port-based rules if the incoming port is within one of the global port ranges.
1467
- if (
1468
- this.settings.globalPortRanges &&
1469
- isPortInRanges(localPort, this.settings.globalPortRanges)
1470
- ) {
1471
- if (this.settings.forwardAllGlobalRanges) {
1472
- if (
1473
- this.settings.defaultAllowedIPs &&
1474
- this.settings.defaultAllowedIPs.length > 0 &&
1475
- !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
1476
- ) {
1477
- console.log(
1478
- `[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`
1479
- );
1480
- socket.end();
1481
- return;
1482
- }
1483
- if (this.settings.enableDetailedLogging) {
1484
- console.log(
1485
- `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
1486
- );
1537
+ // Check if this looks like a TLS handshake
1538
+ const isTlsHandshakeDetected = initialChunk && isTlsHandshake(initialChunk);
1539
+ if (isTlsHandshakeDetected) {
1540
+ connectionRecord.isTLS = true;
1541
+
1542
+ if (this.settings.enableTlsDebugLogging) {
1543
+ console.log(
1544
+ `[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
1545
+ );
1546
+ }
1487
1547
  }
1488
- setupConnection(
1489
- '',
1490
- undefined,
1491
- {
1492
- domains: ['global'],
1493
- allowedIPs: this.settings.defaultAllowedIPs || [],
1494
- blockedIPs: this.settings.defaultBlockedIPs || [],
1495
- targetIPs: [this.settings.targetIP!],
1496
- portRanges: [],
1497
- },
1498
- localPort
1499
- );
1500
- return;
1501
- } else {
1502
- // Attempt to find a matching forced domain config based on the local port.
1503
- const forcedDomain = this.settings.domainConfigs.find(
1504
- (domain) =>
1505
- domain.portRanges &&
1506
- domain.portRanges.length > 0 &&
1507
- isPortInRanges(localPort, domain.portRanges)
1508
- );
1509
- if (forcedDomain) {
1548
+
1549
+ // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
1550
+ const domainConfig = forcedDomain
1551
+ ? forcedDomain
1552
+ : serverName
1553
+ ? this.settings.domainConfigs.find((config) =>
1554
+ config.domains.some((d) => plugins.minimatch(serverName, d))
1555
+ )
1556
+ : undefined;
1557
+
1558
+ // Save domain config in connection record
1559
+ connectionRecord.domainConfig = domainConfig;
1560
+
1561
+ // IP validation is skipped if allowedIPs is empty
1562
+ if (domainConfig) {
1510
1563
  const effectiveAllowedIPs: string[] = [
1511
- ...forcedDomain.allowedIPs,
1564
+ ...domainConfig.allowedIPs,
1512
1565
  ...(this.settings.defaultAllowedIPs || []),
1513
1566
  ];
1514
1567
  const effectiveBlockedIPs: string[] = [
1515
- ...(forcedDomain.blockedIPs || []),
1568
+ ...(domainConfig.blockedIPs || []),
1516
1569
  ...(this.settings.defaultBlockedIPs || []),
1517
1570
  ];
1518
- if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
1519
- console.log(
1520
- `[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
1571
+
1572
+ // Skip IP validation if allowedIPs is empty
1573
+ if (
1574
+ domainConfig.allowedIPs.length > 0 &&
1575
+ !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)
1576
+ ) {
1577
+ return rejectIncomingConnection(
1578
+ 'rejected',
1579
+ `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(
1521
1580
  ', '
1522
- )} on port ${localPort}.`
1581
+ )}`
1582
+ );
1583
+ }
1584
+ } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
1585
+ if (
1586
+ !isGlobIPAllowed(
1587
+ remoteIP,
1588
+ this.settings.defaultAllowedIPs,
1589
+ this.settings.defaultBlockedIPs || []
1590
+ )
1591
+ ) {
1592
+ return rejectIncomingConnection(
1593
+ 'rejected',
1594
+ `Connection rejected: IP ${remoteIP} not allowed by default allowed list`
1595
+ );
1596
+ }
1597
+ }
1598
+
1599
+ // Save the initial SNI
1600
+ if (serverName) {
1601
+ connectionRecord.lockedDomain = serverName;
1602
+ }
1603
+
1604
+ // Set up the direct connection
1605
+ return this.setupDirectConnection(
1606
+ connectionId,
1607
+ socket,
1608
+ connectionRecord,
1609
+ domainConfig,
1610
+ serverName,
1611
+ initialChunk,
1612
+ overridePort
1613
+ );
1614
+ };
1615
+
1616
+ // --- PORT RANGE-BASED HANDLING ---
1617
+ // Only apply port-based rules if the incoming port is within one of the global port ranges.
1618
+ if (
1619
+ this.settings.globalPortRanges &&
1620
+ isPortInRanges(localPort, this.settings.globalPortRanges)
1621
+ ) {
1622
+ if (this.settings.forwardAllGlobalRanges) {
1623
+ if (
1624
+ this.settings.defaultAllowedIPs &&
1625
+ this.settings.defaultAllowedIPs.length > 0 &&
1626
+ !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
1627
+ ) {
1628
+ console.log(
1629
+ `[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`
1523
1630
  );
1524
1631
  socket.end();
1525
1632
  return;
1526
1633
  }
1527
1634
  if (this.settings.enableDetailedLogging) {
1528
1635
  console.log(
1529
- `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
1530
- ', '
1531
- )}.`
1636
+ `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
1532
1637
  );
1533
1638
  }
1534
- setupConnection('', undefined, forcedDomain, localPort);
1639
+ setupConnection(
1640
+ '',
1641
+ undefined,
1642
+ {
1643
+ domains: ['global'],
1644
+ allowedIPs: this.settings.defaultAllowedIPs || [],
1645
+ blockedIPs: this.settings.defaultBlockedIPs || [],
1646
+ targetIPs: [this.settings.targetIP!],
1647
+ portRanges: [],
1648
+ },
1649
+ localPort
1650
+ );
1535
1651
  return;
1652
+ } else {
1653
+ // Attempt to find a matching forced domain config based on the local port.
1654
+ const forcedDomain = this.settings.domainConfigs.find(
1655
+ (domain) =>
1656
+ domain.portRanges &&
1657
+ domain.portRanges.length > 0 &&
1658
+ isPortInRanges(localPort, domain.portRanges)
1659
+ );
1660
+ if (forcedDomain) {
1661
+ const effectiveAllowedIPs: string[] = [
1662
+ ...forcedDomain.allowedIPs,
1663
+ ...(this.settings.defaultAllowedIPs || []),
1664
+ ];
1665
+ const effectiveBlockedIPs: string[] = [
1666
+ ...(forcedDomain.blockedIPs || []),
1667
+ ...(this.settings.defaultBlockedIPs || []),
1668
+ ];
1669
+ if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
1670
+ console.log(
1671
+ `[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
1672
+ ', '
1673
+ )} on port ${localPort}.`
1674
+ );
1675
+ socket.end();
1676
+ return;
1677
+ }
1678
+ if (this.settings.enableDetailedLogging) {
1679
+ console.log(
1680
+ `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
1681
+ ', '
1682
+ )}.`
1683
+ );
1684
+ }
1685
+ setupConnection('', undefined, forcedDomain, localPort);
1686
+ return;
1687
+ }
1688
+ // Fall through to SNI/default handling if no forced domain config is found.
1536
1689
  }
1537
- // Fall through to SNI/default handling if no forced domain config is found.
1538
1690
  }
1539
- }
1540
1691
 
1541
- // --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
1542
- if (this.settings.sniEnabled) {
1543
- initialDataReceived = false;
1692
+ // --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
1693
+ if (this.settings.sniEnabled) {
1694
+ initialDataReceived = false;
1544
1695
 
1545
- socket.once('data', (chunk: Buffer) => {
1546
- if (initialTimeout) {
1547
- clearTimeout(initialTimeout);
1548
- initialTimeout = null;
1549
- }
1696
+ socket.once('data', (chunk: Buffer) => {
1697
+ if (initialTimeout) {
1698
+ clearTimeout(initialTimeout);
1699
+ initialTimeout = null;
1700
+ }
1550
1701
 
1551
- initialDataReceived = true;
1702
+ initialDataReceived = true;
1552
1703
 
1553
- // Try to extract SNI
1554
- let serverName = '';
1704
+ // Try to extract SNI
1705
+ let serverName = '';
1555
1706
 
1556
- if (isTlsHandshake(chunk)) {
1557
- connectionRecord.isTLS = true;
1707
+ if (isTlsHandshake(chunk)) {
1708
+ connectionRecord.isTLS = true;
1558
1709
 
1559
- if (this.settings.enableTlsDebugLogging) {
1710
+ if (this.settings.enableTlsDebugLogging) {
1711
+ console.log(
1712
+ `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
1713
+ );
1714
+ }
1715
+
1716
+ serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
1717
+ }
1718
+
1719
+ // Lock the connection to the negotiated SNI.
1720
+ connectionRecord.lockedDomain = serverName;
1721
+
1722
+ if (this.settings.enableDetailedLogging) {
1560
1723
  console.log(
1561
- `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
1724
+ `[${connectionId}] Received connection from ${remoteIP} with SNI: ${
1725
+ serverName || '(empty)'
1726
+ }`
1562
1727
  );
1563
1728
  }
1564
1729
 
1565
- serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
1566
- }
1567
-
1568
- // Lock the connection to the negotiated SNI.
1569
- connectionRecord.lockedDomain = serverName;
1730
+ setupConnection(serverName, chunk);
1731
+ });
1732
+ } else {
1733
+ initialDataReceived = true;
1734
+ connectionRecord.hasReceivedInitialData = true;
1570
1735
 
1571
- if (this.settings.enableDetailedLogging) {
1572
- console.log(
1573
- `[${connectionId}] Received connection from ${remoteIP} with SNI: ${
1574
- serverName || '(empty)'
1575
- }`
1736
+ if (
1737
+ this.settings.defaultAllowedIPs &&
1738
+ this.settings.defaultAllowedIPs.length > 0 &&
1739
+ !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
1740
+ ) {
1741
+ return rejectIncomingConnection(
1742
+ 'rejected',
1743
+ `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`
1576
1744
  );
1577
1745
  }
1578
1746
 
1579
- setupConnection(serverName, chunk);
1580
- });
1581
- } else {
1582
- initialDataReceived = true;
1583
- connectionRecord.hasReceivedInitialData = true;
1584
-
1585
- if (
1586
- this.settings.defaultAllowedIPs &&
1587
- this.settings.defaultAllowedIPs.length > 0 &&
1588
- !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
1589
- ) {
1590
- return rejectIncomingConnection(
1591
- 'rejected',
1592
- `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`
1593
- );
1747
+ setupConnection('');
1594
1748
  }
1595
-
1596
- setupConnection('');
1597
1749
  }
1598
1750
  };
1599
1751
 
@@ -1619,10 +1771,11 @@ export class PortProxy {
1619
1771
  console.log(`Server Error on port ${port}: ${err.message}`);
1620
1772
  });
1621
1773
  server.listen(port, () => {
1774
+ const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
1622
1775
  console.log(
1623
1776
  `PortProxy -> OK: Now listening on port ${port}${
1624
- this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
1625
- }${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}`
1777
+ this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
1778
+ }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
1626
1779
  );
1627
1780
  });
1628
1781
  this.netServers.push(server);
@@ -1642,6 +1795,7 @@ export class PortProxy {
1642
1795
  let pendingTlsHandshakes = 0;
1643
1796
  let keepAliveConnections = 0;
1644
1797
  let networkProxyConnections = 0;
1798
+ let domainSwitchedConnections = 0;
1645
1799
 
1646
1800
  // Create a copy of the keys to avoid modification during iteration
1647
1801
  const connectionIds = [...this.connectionRecords.keys()];
@@ -1661,20 +1815,23 @@ export class PortProxy {
1661
1815
  } else {
1662
1816
  nonTlsConnections++;
1663
1817
  }
1664
-
1818
+
1665
1819
  if (record.hasKeepAlive) {
1666
1820
  keepAliveConnections++;
1667
1821
  }
1668
-
1822
+
1669
1823
  if (record.usingNetworkProxy) {
1670
1824
  networkProxyConnections++;
1671
1825
  }
1672
1826
 
1827
+ if (record.domainSwitches && record.domainSwitches > 0) {
1828
+ domainSwitchedConnections++;
1829
+ }
1830
+
1673
1831
  maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
1674
1832
  if (record.outgoingStartTime) {
1675
1833
  maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
1676
1834
  }
1677
-
1678
1835
  // Parity check: if outgoing socket closed and incoming remains active
1679
1836
  if (
1680
1837
  record.outgoingClosedTime &&
@@ -1706,35 +1863,38 @@ export class PortProxy {
1706
1863
  }
1707
1864
 
1708
1865
  // Skip inactivity check if disabled or for immortal keep-alive connections
1709
- if (!this.settings.disableInactivityCheck &&
1710
- !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
1711
-
1866
+ if (
1867
+ !this.settings.disableInactivityCheck &&
1868
+ !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
1869
+ ) {
1712
1870
  const inactivityTime = now - record.lastActivity;
1713
-
1871
+
1714
1872
  // Use extended timeout for extended-treatment keep-alive connections
1715
1873
  let effectiveTimeout = this.settings.inactivityTimeout!;
1716
1874
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
1717
1875
  const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
1718
1876
  effectiveTimeout = effectiveTimeout * multiplier;
1719
1877
  }
1720
-
1878
+
1721
1879
  if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
1722
1880
  // For keep-alive connections, issue a warning first
1723
1881
  if (record.hasKeepAlive && !record.inactivityWarningIssued) {
1724
1882
  console.log(
1725
- `[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
1726
- `Will close in 10 minutes if no activity.`
1883
+ `[${id}] Warning: Keep-alive connection from ${
1884
+ record.remoteIP
1885
+ } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
1886
+ `Will close in 10 minutes if no activity.`
1727
1887
  );
1728
-
1888
+
1729
1889
  // Set warning flag and add grace period
1730
1890
  record.inactivityWarningIssued = true;
1731
1891
  record.lastActivity = now - (effectiveTimeout - 600000);
1732
-
1892
+
1733
1893
  // Try to stimulate activity with a probe packet
1734
1894
  if (record.outgoing && !record.outgoing.destroyed) {
1735
1895
  try {
1736
1896
  record.outgoing.write(Buffer.alloc(0));
1737
-
1897
+
1738
1898
  if (this.settings.enableDetailedLogging) {
1739
1899
  console.log(`[${id}] Sent probe packet to test keep-alive connection`);
1740
1900
  }
@@ -1746,15 +1906,17 @@ export class PortProxy {
1746
1906
  // For non-keep-alive or after warning, close the connection
1747
1907
  console.log(
1748
1908
  `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
1749
- `for ${plugins.prettyMs(inactivityTime)}.` +
1750
- (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
1909
+ `for ${plugins.prettyMs(inactivityTime)}.` +
1910
+ (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
1751
1911
  );
1752
1912
  this.cleanupConnection(record, 'inactivity');
1753
1913
  }
1754
1914
  } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
1755
1915
  // If activity detected after warning, clear the warning
1756
1916
  if (this.settings.enableDetailedLogging) {
1757
- console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`);
1917
+ console.log(
1918
+ `[${id}] Connection activity detected after inactivity warning, resetting warning`
1919
+ );
1758
1920
  }
1759
1921
  record.inactivityWarningIssued = false;
1760
1922
  }
@@ -1765,7 +1927,8 @@ export class PortProxy {
1765
1927
  console.log(
1766
1928
  `Active connections: ${this.connectionRecords.size}. ` +
1767
1929
  `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
1768
- `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
1930
+ `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}, ` +
1931
+ `DomainSwitched=${domainSwitchedConnections}. ` +
1769
1932
  `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
1770
1933
  maxOutgoing
1771
1934
  )}. ` +
@@ -1782,21 +1945,6 @@ export class PortProxy {
1782
1945
  }
1783
1946
  }
1784
1947
 
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
-
1800
1948
  /**
1801
1949
  * Gracefully shut down the proxy
1802
1950
  */
@@ -1888,6 +2036,16 @@ export class PortProxy {
1888
2036
  }
1889
2037
  }
1890
2038
 
2039
+ // Stop NetworkProxy if it was started
2040
+ if (this.networkProxy) {
2041
+ try {
2042
+ await this.networkProxy.stop();
2043
+ console.log('NetworkProxy stopped successfully');
2044
+ } catch (err) {
2045
+ console.log(`Error stopping NetworkProxy: ${err}`);
2046
+ }
2047
+ }
2048
+
1891
2049
  // Clear all tracking maps
1892
2050
  this.connectionRecords.clear();
1893
2051
  this.domainTargetIndices.clear();