@push.rocks/smartproxy 3.28.5 → 3.29.0

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