@push.rocks/smartproxy 3.32.2 → 3.33.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,7 +10,7 @@ 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
-
13
+
14
14
  // New properties for NetworkProxy integration
15
15
  useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
16
16
  networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
@@ -54,14 +54,19 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
54
54
  // Rate limiting and security
55
55
  maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
56
56
  connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
57
-
57
+
58
58
  // Enhanced keep-alive settings
59
59
  keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
60
60
  keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
61
61
  extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
62
-
62
+
63
63
  // New property for NetworkProxy integration
64
64
  networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
65
+
66
+ // Browser optimization settings
67
+ browserFriendlyMode?: boolean; // Optimizes handling for browser connections
68
+ allowRenegotiationWithDifferentSNI?: boolean; // Allows SNI changes during renegotiation
69
+ relatedDomainPatterns?: string[][]; // Patterns for domains that should be allowed to share connections
65
70
  }
66
71
 
67
72
  /**
@@ -90,16 +95,23 @@ interface IConnectionRecord {
90
95
  tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
91
96
  hasReceivedInitialData: boolean; // Whether initial data has been received
92
97
  domainConfig?: IDomainConfig; // Associated domain config for this connection
93
-
98
+
94
99
  // Keep-alive tracking
95
100
  hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
96
101
  inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
97
102
  incomingTerminationReason?: string | null; // Reason for incoming termination
98
103
  outgoingTerminationReason?: string | null; // Reason for outgoing termination
99
-
104
+
100
105
  // New field for NetworkProxy tracking
101
106
  usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
102
107
  networkProxyIndex?: number; // Which NetworkProxy instance is being used
108
+
109
+ // New field for renegotiation handler
110
+ renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
111
+
112
+ // Browser connection tracking
113
+ isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
114
+ domainSwitches?: number; // Number of times the domain has been switched on this connection
103
115
  }
104
116
 
105
117
  /**
@@ -266,6 +278,58 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
266
278
  }
267
279
  }
268
280
 
281
+ /**
282
+ * Checks if a TLS record is a proper ClientHello message (more accurate than just checking record type)
283
+ * @param buffer - Buffer containing the TLS record
284
+ * @returns true if the buffer contains a proper ClientHello message
285
+ */
286
+ function isClientHello(buffer: Buffer): boolean {
287
+ try {
288
+ if (buffer.length < 9) return false; // Too small for a proper ClientHello
289
+
290
+ // Check record type (has to be handshake - 22)
291
+ if (buffer.readUInt8(0) !== 22) return false;
292
+
293
+ // After the TLS record header (5 bytes), check the handshake type (1 for ClientHello)
294
+ if (buffer.readUInt8(5) !== 1) return false;
295
+
296
+ // Basic checks passed, this appears to be a ClientHello
297
+ return true;
298
+ } catch (err) {
299
+ console.log(`Error checking for ClientHello: ${err}`);
300
+ return false;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Checks if two domains are related based on configured patterns
306
+ * @param domain1 - First domain name
307
+ * @param domain2 - Second domain name
308
+ * @param relatedPatterns - Array of domain pattern groups where domains in the same group are considered related
309
+ * @returns true if domains are related, false otherwise
310
+ */
311
+ function areDomainsRelated(
312
+ domain1: string,
313
+ domain2: string,
314
+ relatedPatterns?: string[][]
315
+ ): boolean {
316
+ // Only exact same domains or empty domains are automatically related
317
+ if (!domain1 || !domain2 || domain1 === domain2) return true;
318
+
319
+ // Check against configured related domain patterns - the ONLY source of truth
320
+ if (relatedPatterns && relatedPatterns.length > 0) {
321
+ for (const patternGroup of relatedPatterns) {
322
+ const domain1Matches = patternGroup.some((pattern) => plugins.minimatch(domain1, pattern));
323
+ const domain2Matches = patternGroup.some((pattern) => plugins.minimatch(domain2, pattern));
324
+
325
+ if (domain1Matches && domain2Matches) return true;
326
+ }
327
+ }
328
+
329
+ // If no patterns match, domains are not related
330
+ return false;
331
+ }
332
+
269
333
  // Helper: Check if a port falls within any of the given port ranges
270
334
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
271
335
  return ranges.some((range) => port >= range.from && port <= range.to);
@@ -348,7 +412,7 @@ export class PortProxy {
348
412
  // Connection tracking by IP for rate limiting
349
413
  private connectionsByIP: Map<string, Set<string>> = new Map();
350
414
  private connectionRateByIP: Map<string, number[]> = new Map();
351
-
415
+
352
416
  // New property to store NetworkProxy instances
353
417
  private networkProxies: NetworkProxy[] = [];
354
418
 
@@ -375,8 +439,8 @@ export class PortProxy {
375
439
 
376
440
  // Feature flags
377
441
  disableInactivityCheck: settingsArg.disableInactivityCheck || false,
378
- enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
379
- ? settingsArg.enableKeepAliveProbes : true, // Enable by default
442
+ enableKeepAliveProbes:
443
+ settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, // Enable by default
380
444
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
381
445
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
382
446
  enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
@@ -384,13 +448,18 @@ export class PortProxy {
384
448
  // Rate limiting defaults
385
449
  maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
386
450
  connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
387
-
451
+
388
452
  // Enhanced keep-alive settings
389
453
  keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
390
454
  keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
391
455
  extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
456
+
457
+ // Browser optimization settings (new)
458
+ browserFriendlyMode: settingsArg.browserFriendlyMode || true, // On by default
459
+ allowRenegotiationWithDifferentSNI: settingsArg.allowRenegotiationWithDifferentSNI || false, // Off by default
460
+ relatedDomainPatterns: settingsArg.relatedDomainPatterns || [], // Empty by default
392
461
  };
393
-
462
+
394
463
  // Store NetworkProxy instances if provided
395
464
  this.networkProxies = settingsArg.networkProxies || [];
396
465
  }
@@ -413,58 +482,66 @@ export class PortProxy {
413
482
  serverName?: string
414
483
  ): void {
415
484
  // Determine which NetworkProxy to use
416
- const proxyIndex = domainConfig.networkProxyIndex !== undefined
417
- ? domainConfig.networkProxyIndex
418
- : 0;
419
-
485
+ const proxyIndex =
486
+ domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
487
+
420
488
  // Validate the NetworkProxy index
421
489
  if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
422
- console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`);
490
+ console.log(
491
+ `[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
492
+ );
423
493
  // Fall back to direct connection
424
- return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData);
494
+ return this.setupDirectConnection(
495
+ connectionId,
496
+ socket,
497
+ record,
498
+ domainConfig,
499
+ serverName,
500
+ initialData
501
+ );
425
502
  }
426
-
503
+
427
504
  const networkProxy = this.networkProxies[proxyIndex];
428
505
  const proxyPort = networkProxy.getListeningPort();
429
506
  const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
430
-
507
+
431
508
  if (this.settings.enableDetailedLogging) {
432
509
  console.log(
433
510
  `[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
434
511
  );
435
512
  }
436
-
513
+
437
514
  // Create a connection to the NetworkProxy
438
515
  const proxySocket = plugins.net.connect({
439
516
  host: proxyHost,
440
- port: proxyPort
517
+ port: proxyPort,
441
518
  });
442
-
519
+
443
520
  // Store the outgoing socket in the record
444
521
  record.outgoing = proxySocket;
445
522
  record.outgoingStartTime = Date.now();
446
523
  record.usingNetworkProxy = true;
447
524
  record.networkProxyIndex = proxyIndex;
448
-
525
+
449
526
  // Set up error handlers
450
527
  proxySocket.on('error', (err) => {
451
528
  console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
452
529
  this.cleanupConnection(record, 'network_proxy_connect_error');
453
530
  });
454
-
531
+
455
532
  // Handle connection to NetworkProxy
456
533
  proxySocket.on('connect', () => {
457
534
  if (this.settings.enableDetailedLogging) {
458
535
  console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
459
536
  }
460
-
537
+
461
538
  // First send the initial data that contains the TLS ClientHello
462
539
  proxySocket.write(initialData);
463
-
540
+
464
541
  // Now set up bidirectional piping between client and NetworkProxy
465
542
  socket.pipe(proxySocket);
466
543
  proxySocket.pipe(socket);
467
-
544
+
468
545
  // Setup cleanup handlers
469
546
  proxySocket.on('close', () => {
470
547
  if (this.settings.enableDetailedLogging) {
@@ -472,18 +549,20 @@ export class PortProxy {
472
549
  }
473
550
  this.cleanupConnection(record, 'network_proxy_closed');
474
551
  });
475
-
552
+
476
553
  socket.on('close', () => {
477
554
  if (this.settings.enableDetailedLogging) {
478
- console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`);
555
+ console.log(
556
+ `[${connectionId}] Client connection closed after forwarding to NetworkProxy`
557
+ );
479
558
  }
480
559
  this.cleanupConnection(record, 'client_closed');
481
560
  });
482
-
561
+
483
562
  // Update activity on data transfer
484
563
  socket.on('data', () => this.updateActivity(record));
485
564
  proxySocket.on('data', () => this.updateActivity(record));
486
-
565
+
487
566
  if (this.settings.enableDetailedLogging) {
488
567
  console.log(
489
568
  `[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
@@ -491,7 +570,7 @@ export class PortProxy {
491
570
  }
492
571
  });
493
572
  }
494
-
573
+
495
574
  /**
496
575
  * Sets up a direct connection to the target (original behavior)
497
576
  * This is used when NetworkProxy isn't configured or as a fallback
@@ -568,11 +647,11 @@ export class PortProxy {
568
647
 
569
648
  // Apply socket optimizations
570
649
  targetSocket.setNoDelay(this.settings.noDelay);
571
-
650
+
572
651
  // Apply keep-alive settings to the outgoing connection as well
573
652
  if (this.settings.keepAlive) {
574
653
  targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
575
-
654
+
576
655
  // Apply enhanced TCP keep-alive options if enabled
577
656
  if (this.settings.enableKeepAliveProbes) {
578
657
  try {
@@ -585,7 +664,9 @@ export class PortProxy {
585
664
  } catch (err) {
586
665
  // Ignore errors - these are optional enhancements
587
666
  if (this.settings.enableDetailedLogging) {
588
- console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
667
+ console.log(
668
+ `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
669
+ );
589
670
  }
590
671
  }
591
672
  }
@@ -642,19 +723,21 @@ export class PortProxy {
642
723
  // For keep-alive connections, just log a warning instead of closing
643
724
  if (record.hasKeepAlive) {
644
725
  console.log(
645
- `[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
726
+ `[${connectionId}] Timeout event on incoming keep-alive connection from ${
727
+ record.remoteIP
728
+ } after ${plugins.prettyMs(
646
729
  this.settings.socketTimeout || 3600000
647
730
  )}. Connection preserved.`
648
731
  );
649
732
  // Don't close the connection - just log
650
733
  return;
651
734
  }
652
-
735
+
653
736
  // For non-keep-alive connections, proceed with normal cleanup
654
737
  console.log(
655
- `[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(
656
- this.settings.socketTimeout || 3600000
657
- )}`
738
+ `[${connectionId}] Timeout on incoming side from ${
739
+ record.remoteIP
740
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
658
741
  );
659
742
  if (record.incomingTerminationReason === null) {
660
743
  record.incomingTerminationReason = 'timeout';
@@ -667,19 +750,21 @@ export class PortProxy {
667
750
  // For keep-alive connections, just log a warning instead of closing
668
751
  if (record.hasKeepAlive) {
669
752
  console.log(
670
- `[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
753
+ `[${connectionId}] Timeout event on outgoing keep-alive connection from ${
754
+ record.remoteIP
755
+ } after ${plugins.prettyMs(
671
756
  this.settings.socketTimeout || 3600000
672
757
  )}. Connection preserved.`
673
758
  );
674
759
  // Don't close the connection - just log
675
760
  return;
676
761
  }
677
-
762
+
678
763
  // For non-keep-alive connections, proceed with normal cleanup
679
764
  console.log(
680
- `[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(
681
- this.settings.socketTimeout || 3600000
682
- )}`
765
+ `[${connectionId}] Timeout on outgoing side from ${
766
+ record.remoteIP
767
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
683
768
  );
684
769
  if (record.outgoingTerminationReason === null) {
685
770
  record.outgoingTerminationReason = 'timeout';
@@ -693,9 +778,11 @@ export class PortProxy {
693
778
  // Disable timeouts completely for immortal connections
694
779
  socket.setTimeout(0);
695
780
  targetSocket.setTimeout(0);
696
-
781
+
697
782
  if (this.settings.enableDetailedLogging) {
698
- console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
783
+ console.log(
784
+ `[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`
785
+ );
699
786
  }
700
787
  } else {
701
788
  // Set normal timeouts for other connections
@@ -725,9 +812,7 @@ export class PortProxy {
725
812
  const combinedData = Buffer.concat(record.pendingData);
726
813
  targetSocket.write(combinedData, (err) => {
727
814
  if (err) {
728
- console.log(
729
- `[${connectionId}] Error writing pending data to target: ${err.message}`
730
- );
815
+ console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
731
816
  return this.initiateCleanupOnce(record, 'write_error');
732
817
  }
733
818
 
@@ -746,7 +831,9 @@ export class PortProxy {
746
831
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
747
832
  : ''
748
833
  }` +
749
- ` 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
+ }`
750
837
  );
751
838
  } else {
752
839
  console.log(
@@ -777,7 +864,9 @@ export class PortProxy {
777
864
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
778
865
  : ''
779
866
  }` +
780
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
867
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
868
+ record.hasKeepAlive ? 'Yes' : 'No'
869
+ }`
781
870
  );
782
871
  } else {
783
872
  console.log(
@@ -797,82 +886,134 @@ export class PortProxy {
797
886
  record.pendingData = [];
798
887
  record.pendingDataSize = 0;
799
888
 
800
- // Add the renegotiation listener for SNI validation
889
+ // Add the renegotiation handler for SNI validation, with browser-friendly improvements
801
890
  if (serverName) {
802
- socket.on('data', (renegChunk: Buffer) => {
803
- if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
891
+ // Define a handler for checking renegotiation with improved detection
892
+ const renegotiationHandler = (renegChunk: Buffer) => {
893
+ // Only process if this looks like a TLS ClientHello (more precise than just checking for type 22)
894
+ if (isClientHello(renegChunk)) {
804
895
  try {
805
- // Try to extract SNI from potential renegotiation
896
+ // Extract SNI from ClientHello
806
897
  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.`
898
+
899
+ // Skip if no SNI was found
900
+ if (!newSNI) return;
901
+
902
+ // Handle SNI change during renegotiation
903
+ if (newSNI !== record.lockedDomain) {
904
+ // Track domain switches for browser connections
905
+ if (!record.domainSwitches) record.domainSwitches = 0;
906
+ record.domainSwitches++;
907
+
908
+ // Check if this is a normal behavior of browser connection reuse
909
+ const isRelatedDomain = areDomainsRelated(
910
+ newSNI,
911
+ record.lockedDomain || '',
912
+ this.settings.relatedDomainPatterns
810
913
  );
811
- this.initiateCleanupOnce(record, 'sni_mismatch');
812
- } else if (newSNI && this.settings.enableDetailedLogging) {
914
+
915
+ // Decide how to handle the SNI change based on settings
916
+ if (this.settings.browserFriendlyMode && isRelatedDomain) {
917
+ console.log(
918
+ `[${connectionId}] Browser domain switch detected: ${record.lockedDomain} -> ${newSNI}. ` +
919
+ `Domains are related, allowing connection to continue (domain switch #${record.domainSwitches}).`
920
+ );
921
+
922
+ // Update the locked domain to the new one
923
+ record.lockedDomain = newSNI;
924
+ } else if (this.settings.allowRenegotiationWithDifferentSNI) {
925
+ console.log(
926
+ `[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
927
+ `Allowing due to allowRenegotiationWithDifferentSNI setting.`
928
+ );
929
+
930
+ // Update the locked domain to the new one
931
+ record.lockedDomain = newSNI;
932
+ } else {
933
+ // Standard strict behavior - terminate connection on SNI mismatch
934
+ console.log(
935
+ `[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
936
+ `Terminating connection. Enable browserFriendlyMode to allow this.`
937
+ );
938
+ this.initiateCleanupOnce(record, 'sni_mismatch');
939
+ }
940
+ } else if (this.settings.enableDetailedLogging) {
813
941
  console.log(
814
- `[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
942
+ `[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
815
943
  );
816
944
  }
817
945
  } catch (err) {
818
946
  console.log(
819
- `[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
947
+ `[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
820
948
  );
821
949
  }
822
950
  }
823
- });
951
+ };
952
+
953
+ // Store the handler in the connection record so we can remove it during cleanup
954
+ record.renegotiationHandler = renegotiationHandler;
955
+
956
+ // Add the listener
957
+ socket.on('data', renegotiationHandler);
824
958
  }
825
959
 
826
960
  // Set connection timeout with simpler logic
827
961
  if (record.cleanupTimer) {
828
962
  clearTimeout(record.cleanupTimer);
829
963
  }
830
-
964
+
831
965
  // For immortal keep-alive connections, skip setting a timeout completely
832
966
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
833
967
  if (this.settings.enableDetailedLogging) {
834
- console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
968
+ console.log(
969
+ `[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
970
+ );
835
971
  }
836
972
  // No cleanup timer for immortal connections
837
- }
973
+ }
838
974
  // For extended keep-alive connections, use extended timeout
839
975
  else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
840
976
  const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
841
977
  const safeTimeout = ensureSafeTimeout(extendedTimeout);
842
-
978
+
843
979
  record.cleanupTimer = setTimeout(() => {
844
980
  console.log(
845
- `[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs(
846
- extendedTimeout
847
- )}), forcing cleanup.`
981
+ `[${connectionId}] Keep-alive connection from ${
982
+ record.remoteIP
983
+ } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
848
984
  );
849
985
  this.initiateCleanupOnce(record, 'extended_lifetime');
850
986
  }, safeTimeout);
851
-
987
+
852
988
  // Make sure timeout doesn't keep the process alive
853
989
  if (record.cleanupTimer.unref) {
854
990
  record.cleanupTimer.unref();
855
991
  }
856
-
992
+
857
993
  if (this.settings.enableDetailedLogging) {
858
- console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
994
+ console.log(
995
+ `[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
996
+ extendedTimeout
997
+ )}`
998
+ );
859
999
  }
860
1000
  }
861
1001
  // For standard connections, use normal timeout
862
1002
  else {
863
1003
  // Use domain-specific timeout if available, otherwise use default
864
- const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
1004
+ const connectionTimeout =
1005
+ record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
865
1006
  const safeTimeout = ensureSafeTimeout(connectionTimeout);
866
-
1007
+
867
1008
  record.cleanupTimer = setTimeout(() => {
868
1009
  console.log(
869
- `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs(
870
- connectionTimeout
871
- )}), forcing cleanup.`
1010
+ `[${connectionId}] Connection from ${
1011
+ record.remoteIP
1012
+ } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
872
1013
  );
873
1014
  this.initiateCleanupOnce(record, 'connection_timeout');
874
1015
  }, safeTimeout);
875
-
1016
+
876
1017
  // Make sure timeout doesn't keep the process alive
877
1018
  if (record.cleanupTimer.unref) {
878
1019
  record.cleanupTimer.unref();
@@ -973,6 +1114,16 @@ export class PortProxy {
973
1114
  const bytesReceived = record.bytesReceived;
974
1115
  const bytesSent = record.bytesSent;
975
1116
 
1117
+ // Remove the renegotiation handler if present
1118
+ if (record.renegotiationHandler && record.incoming) {
1119
+ try {
1120
+ record.incoming.removeListener('data', record.renegotiationHandler);
1121
+ record.renegotiationHandler = undefined;
1122
+ } catch (err) {
1123
+ console.log(`[${record.id}] Error removing renegotiation handler: ${err}`);
1124
+ }
1125
+ }
1126
+
976
1127
  try {
977
1128
  if (!record.incoming.destroyed) {
978
1129
  // Try graceful shutdown first, then force destroy after a short timeout
@@ -1047,8 +1198,11 @@ export class PortProxy {
1047
1198
  ` Duration: ${plugins.prettyMs(
1048
1199
  duration
1049
1200
  )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
1050
- `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
1051
- `${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
1201
+ `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1202
+ record.hasKeepAlive ? 'Yes' : 'No'
1203
+ }` +
1204
+ `${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}` +
1205
+ `${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
1052
1206
  );
1053
1207
  } else {
1054
1208
  console.log(
@@ -1063,7 +1217,7 @@ export class PortProxy {
1063
1217
  */
1064
1218
  private updateActivity(record: IConnectionRecord): void {
1065
1219
  record.lastActivity = Date.now();
1066
-
1220
+
1067
1221
  // Clear any inactivity warning
1068
1222
  if (record.inactivityWarningIssued) {
1069
1223
  record.inactivityWarningIssued = false;
@@ -1082,7 +1236,7 @@ export class PortProxy {
1082
1236
  }
1083
1237
  return this.settings.targetIP!;
1084
1238
  }
1085
-
1239
+
1086
1240
  /**
1087
1241
  * Initiates cleanup once for a connection
1088
1242
  */
@@ -1090,12 +1244,15 @@ export class PortProxy {
1090
1244
  if (this.settings.enableDetailedLogging) {
1091
1245
  console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
1092
1246
  }
1093
-
1094
- if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) {
1247
+
1248
+ if (
1249
+ record.incomingTerminationReason === null ||
1250
+ record.incomingTerminationReason === undefined
1251
+ ) {
1095
1252
  record.incomingTerminationReason = reason;
1096
1253
  this.incrementTerminationStat('incoming', reason);
1097
1254
  }
1098
-
1255
+
1099
1256
  this.cleanupConnection(record, reason);
1100
1257
  }
1101
1258
 
@@ -1219,7 +1376,7 @@ export class PortProxy {
1219
1376
 
1220
1377
  // Apply socket optimizations
1221
1378
  socket.setNoDelay(this.settings.noDelay);
1222
-
1379
+
1223
1380
  // Create a unique connection ID and record
1224
1381
  const connectionId = generateConnectionId();
1225
1382
  const connectionRecord: IConnectionRecord = {
@@ -1243,16 +1400,20 @@ export class PortProxy {
1243
1400
  hasKeepAlive: false, // Will set to true if keep-alive is applied
1244
1401
  incomingTerminationReason: null,
1245
1402
  outgoingTerminationReason: null,
1246
-
1403
+
1247
1404
  // Initialize NetworkProxy tracking fields
1248
- usingNetworkProxy: false
1405
+ usingNetworkProxy: false,
1406
+
1407
+ // Initialize browser connection tracking
1408
+ isBrowserConnection: this.settings.browserFriendlyMode, // Assume browser if browserFriendlyMode is enabled
1409
+ domainSwitches: 0, // Track domain switches
1249
1410
  };
1250
-
1411
+
1251
1412
  // Apply keep-alive settings if enabled
1252
1413
  if (this.settings.keepAlive) {
1253
1414
  socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
1254
1415
  connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
1255
-
1416
+
1256
1417
  // Apply enhanced TCP keep-alive options if enabled
1257
1418
  if (this.settings.enableKeepAliveProbes) {
1258
1419
  try {
@@ -1266,7 +1427,9 @@ export class PortProxy {
1266
1427
  } catch (err) {
1267
1428
  // Ignore errors - these are optional enhancements
1268
1429
  if (this.settings.enableDetailedLogging) {
1269
- console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
1430
+ console.log(
1431
+ `[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
1432
+ );
1270
1433
  }
1271
1434
  }
1272
1435
  }
@@ -1279,8 +1442,9 @@ export class PortProxy {
1279
1442
  if (this.settings.enableDetailedLogging) {
1280
1443
  console.log(
1281
1444
  `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
1282
- `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
1283
- `Active connections: ${this.connectionRecords.size}`
1445
+ `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
1446
+ `Mode: ${this.settings.browserFriendlyMode ? 'Browser-friendly' : 'Standard'}. ` +
1447
+ `Active connections: ${this.connectionRecords.size}`
1284
1448
  );
1285
1449
  } else {
1286
1450
  console.log(
@@ -1418,12 +1582,12 @@ export class PortProxy {
1418
1582
  )}`
1419
1583
  );
1420
1584
  }
1421
-
1585
+
1422
1586
  // Check if we should forward this to a NetworkProxy
1423
1587
  if (
1424
- isTlsHandshakeDetected &&
1425
- domainConfig.useNetworkProxy === true &&
1426
- initialChunk &&
1588
+ isTlsHandshakeDetected &&
1589
+ domainConfig.useNetworkProxy === true &&
1590
+ initialChunk &&
1427
1591
  this.networkProxies.length > 0
1428
1592
  ) {
1429
1593
  return this.forwardToNetworkProxy(
@@ -1450,6 +1614,11 @@ export class PortProxy {
1450
1614
  }
1451
1615
  }
1452
1616
 
1617
+ // Save the initial SNI for browser connection management
1618
+ if (serverName) {
1619
+ connectionRecord.lockedDomain = serverName;
1620
+ }
1621
+
1453
1622
  // If we didn't forward to NetworkProxy, proceed with direct connection
1454
1623
  return this.setupDirectConnection(
1455
1624
  connectionId,
@@ -1622,7 +1791,9 @@ export class PortProxy {
1622
1791
  console.log(
1623
1792
  `PortProxy -> OK: Now listening on port ${port}${
1624
1793
  this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
1625
- }${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}`
1794
+ }${this.networkProxies.length > 0 ? ' (NetworkProxy integration enabled)' : ''}${
1795
+ this.settings.browserFriendlyMode ? ' (Browser-friendly mode enabled)' : ''
1796
+ }`
1626
1797
  );
1627
1798
  });
1628
1799
  this.netServers.push(server);
@@ -1642,6 +1813,7 @@ export class PortProxy {
1642
1813
  let pendingTlsHandshakes = 0;
1643
1814
  let keepAliveConnections = 0;
1644
1815
  let networkProxyConnections = 0;
1816
+ let domainSwitchedConnections = 0;
1645
1817
 
1646
1818
  // Create a copy of the keys to avoid modification during iteration
1647
1819
  const connectionIds = [...this.connectionRecords.keys()];
@@ -1661,20 +1833,23 @@ export class PortProxy {
1661
1833
  } else {
1662
1834
  nonTlsConnections++;
1663
1835
  }
1664
-
1836
+
1665
1837
  if (record.hasKeepAlive) {
1666
1838
  keepAliveConnections++;
1667
1839
  }
1668
-
1840
+
1669
1841
  if (record.usingNetworkProxy) {
1670
1842
  networkProxyConnections++;
1671
1843
  }
1672
1844
 
1845
+ if (record.domainSwitches && record.domainSwitches > 0) {
1846
+ domainSwitchedConnections++;
1847
+ }
1848
+
1673
1849
  maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
1674
1850
  if (record.outgoingStartTime) {
1675
1851
  maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
1676
1852
  }
1677
-
1678
1853
  // Parity check: if outgoing socket closed and incoming remains active
1679
1854
  if (
1680
1855
  record.outgoingClosedTime &&
@@ -1706,35 +1881,38 @@ export class PortProxy {
1706
1881
  }
1707
1882
 
1708
1883
  // Skip inactivity check if disabled or for immortal keep-alive connections
1709
- if (!this.settings.disableInactivityCheck &&
1710
- !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
1711
-
1884
+ if (
1885
+ !this.settings.disableInactivityCheck &&
1886
+ !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
1887
+ ) {
1712
1888
  const inactivityTime = now - record.lastActivity;
1713
-
1889
+
1714
1890
  // Use extended timeout for extended-treatment keep-alive connections
1715
1891
  let effectiveTimeout = this.settings.inactivityTimeout!;
1716
1892
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
1717
1893
  const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
1718
1894
  effectiveTimeout = effectiveTimeout * multiplier;
1719
1895
  }
1720
-
1896
+
1721
1897
  if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
1722
1898
  // For keep-alive connections, issue a warning first
1723
1899
  if (record.hasKeepAlive && !record.inactivityWarningIssued) {
1724
1900
  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.`
1901
+ `[${id}] Warning: Keep-alive connection from ${
1902
+ record.remoteIP
1903
+ } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
1904
+ `Will close in 10 minutes if no activity.`
1727
1905
  );
1728
-
1906
+
1729
1907
  // Set warning flag and add grace period
1730
1908
  record.inactivityWarningIssued = true;
1731
1909
  record.lastActivity = now - (effectiveTimeout - 600000);
1732
-
1910
+
1733
1911
  // Try to stimulate activity with a probe packet
1734
1912
  if (record.outgoing && !record.outgoing.destroyed) {
1735
1913
  try {
1736
1914
  record.outgoing.write(Buffer.alloc(0));
1737
-
1915
+
1738
1916
  if (this.settings.enableDetailedLogging) {
1739
1917
  console.log(`[${id}] Sent probe packet to test keep-alive connection`);
1740
1918
  }
@@ -1746,15 +1924,17 @@ export class PortProxy {
1746
1924
  // For non-keep-alive or after warning, close the connection
1747
1925
  console.log(
1748
1926
  `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
1749
- `for ${plugins.prettyMs(inactivityTime)}.` +
1750
- (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
1927
+ `for ${plugins.prettyMs(inactivityTime)}.` +
1928
+ (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
1751
1929
  );
1752
1930
  this.cleanupConnection(record, 'inactivity');
1753
1931
  }
1754
1932
  } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
1755
1933
  // If activity detected after warning, clear the warning
1756
1934
  if (this.settings.enableDetailedLogging) {
1757
- console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`);
1935
+ console.log(
1936
+ `[${id}] Connection activity detected after inactivity warning, resetting warning`
1937
+ );
1758
1938
  }
1759
1939
  record.inactivityWarningIssued = false;
1760
1940
  }
@@ -1765,7 +1945,8 @@ export class PortProxy {
1765
1945
  console.log(
1766
1946
  `Active connections: ${this.connectionRecords.size}. ` +
1767
1947
  `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
1768
- `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` +
1948
+ `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}, ` +
1949
+ `DomainSwitched=${domainSwitchedConnections}. ` +
1769
1950
  `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
1770
1951
  maxOutgoing
1771
1952
  )}. ` +
@@ -1903,4 +2084,4 @@ export class PortProxy {
1903
2084
 
1904
2085
  console.log('PortProxy shutdown complete.');
1905
2086
  }
1906
- }
2087
+ }