@push.rocks/smartproxy 3.28.3 → 3.28.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartproxy",
3
- "version": "3.28.3",
3
+ "version": "3.28.6",
4
4
  "private": false,
5
5
  "description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
6
6
  "main": "dist_ts/index.js",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '3.28.3',
6
+ version: '3.28.6',
7
7
  description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
8
8
  }
@@ -26,8 +26,8 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
26
26
  initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
27
27
  socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
28
28
  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)
29
+ maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h)
30
+ inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
31
31
 
32
32
  gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
33
33
  globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
@@ -49,6 +49,11 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
49
49
  // Rate limiting and security
50
50
  maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
51
51
  connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
52
+
53
+ // Enhanced keep-alive settings
54
+ keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
55
+ keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
56
+ extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
52
57
  }
53
58
 
54
59
  /**
@@ -77,6 +82,12 @@ interface IConnectionRecord {
77
82
  tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
78
83
  hasReceivedInitialData: boolean; // Whether initial data has been received
79
84
  domainConfig?: IDomainConfig; // Associated domain config for this connection
85
+
86
+ // Keep-alive tracking
87
+ hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
88
+ inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
89
+ incomingTerminationReason?: string | null; // Reason for incoming termination
90
+ outgoingTerminationReason?: string | null; // Reason for outgoing termination
80
91
  }
81
92
 
82
93
  /**
@@ -332,11 +343,11 @@ export class PortProxy {
332
343
  ...settingsArg,
333
344
  targetIP: settingsArg.targetIP || 'localhost',
334
345
 
335
- // Timeout settings with safe maximum values
346
+ // Timeout settings with reasonable defaults
336
347
  initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake
337
- socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 2147483647), // Maximum safe value (~24.8 days)
348
+ socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout
338
349
  inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
339
- maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 2147483647), // Maximum safe value (~24.8 days)
350
+ maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default
340
351
  inactivityTimeout: ensureSafeTimeout(settingsArg.inactivityTimeout || 14400000), // 4 hours inactivity timeout
341
352
 
342
353
  gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
@@ -344,19 +355,25 @@ export class PortProxy {
344
355
  // Socket optimization settings
345
356
  noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
346
357
  keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
347
- keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds
358
+ keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
348
359
  maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
349
360
 
350
361
  // Feature flags
351
362
  disableInactivityCheck: settingsArg.disableInactivityCheck || false,
352
- enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false,
363
+ enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
364
+ ? settingsArg.enableKeepAliveProbes : true, // Enable by default
353
365
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
354
366
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
355
- enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true,
367
+ enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
356
368
 
357
369
  // Rate limiting defaults
358
370
  maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
359
371
  connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
372
+
373
+ // Enhanced keep-alive settings
374
+ keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
375
+ keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
376
+ extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
360
377
  };
361
378
  }
362
379
 
@@ -418,25 +435,6 @@ export class PortProxy {
418
435
  this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
419
436
  }
420
437
 
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
438
  /**
441
439
  * Cleans up a connection record.
442
440
  * Destroys both incoming and outgoing sockets, clears timers, and removes the record.
@@ -534,7 +532,7 @@ export class PortProxy {
534
532
  ` Duration: ${plugins.prettyMs(
535
533
  duration
536
534
  )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
537
- `TLS: ${record.isTLS ? 'Yes' : 'No'}`
535
+ `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
538
536
  );
539
537
  } else {
540
538
  console.log(
@@ -549,6 +547,11 @@ export class PortProxy {
549
547
  */
550
548
  private updateActivity(record: IConnectionRecord): void {
551
549
  record.lastActivity = Date.now();
550
+
551
+ // Clear any inactivity warning
552
+ if (record.inactivityWarningIssued) {
553
+ record.inactivityWarningIssued = false;
554
+ }
552
555
  }
553
556
 
554
557
  /**
@@ -563,6 +566,22 @@ export class PortProxy {
563
566
  }
564
567
  return this.settings.targetIP!;
565
568
  }
569
+
570
+ /**
571
+ * Initiates cleanup once for a connection
572
+ */
573
+ private initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
574
+ if (this.settings.enableDetailedLogging) {
575
+ console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
576
+ }
577
+
578
+ if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) {
579
+ record.incomingTerminationReason = reason;
580
+ this.incrementTerminationStat('incoming', reason);
581
+ }
582
+
583
+ this.cleanupConnection(record, reason);
584
+ }
566
585
 
567
586
  /**
568
587
  * Main method to start the proxy
@@ -609,25 +628,7 @@ export class PortProxy {
609
628
 
610
629
  // Apply socket optimizations
611
630
  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
-
631
+
631
632
  // Create a unique connection ID and record
632
633
  const connectionId = generateConnectionId();
633
634
  const connectionRecord: IConnectionRecord = {
@@ -648,7 +649,34 @@ export class PortProxy {
648
649
  isTLS: false,
649
650
  tlsHandshakeComplete: false,
650
651
  hasReceivedInitialData: false,
652
+ hasKeepAlive: false, // Will set to true if keep-alive is applied
653
+ incomingTerminationReason: null,
654
+ outgoingTerminationReason: null
651
655
  };
656
+
657
+ // Apply keep-alive settings if enabled
658
+ if (this.settings.keepAlive) {
659
+ socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
660
+ connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
661
+
662
+ // Apply enhanced TCP keep-alive options if enabled
663
+ if (this.settings.enableKeepAliveProbes) {
664
+ try {
665
+ // These are platform-specific and may not be available
666
+ if ('setKeepAliveProbes' in socket) {
667
+ (socket as any).setKeepAliveProbes(10); // More aggressive probing
668
+ }
669
+ if ('setKeepAliveInterval' in socket) {
670
+ (socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
671
+ }
672
+ } catch (err) {
673
+ // Ignore errors - these are optional enhancements
674
+ if (this.settings.enableDetailedLogging) {
675
+ console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
676
+ }
677
+ }
678
+ }
679
+ }
652
680
 
653
681
  // Track connection by IP
654
682
  this.trackConnectionByIP(remoteIP, connectionId);
@@ -656,7 +684,9 @@ export class PortProxy {
656
684
 
657
685
  if (this.settings.enableDetailedLogging) {
658
686
  console.log(
659
- `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`
687
+ `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
688
+ `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
689
+ `Active connections: ${this.connectionRecords.size}`
660
690
  );
661
691
  } else {
662
692
  console.log(
@@ -668,11 +698,6 @@ export class PortProxy {
668
698
  let incomingTerminationReason: string | null = null;
669
699
  let outgoingTerminationReason: string | null = null;
670
700
 
671
- // Local function for cleanupOnce
672
- const cleanupOnce = () => {
673
- this.cleanupConnection(connectionRecord);
674
- };
675
-
676
701
  // Define initiateCleanupOnce for compatibility
677
702
  const initiateCleanupOnce = (reason: string = 'normal') => {
678
703
  if (this.settings.enableDetailedLogging) {
@@ -680,9 +705,10 @@ export class PortProxy {
680
705
  }
681
706
  if (incomingTerminationReason === null) {
682
707
  incomingTerminationReason = reason;
708
+ connectionRecord.incomingTerminationReason = reason;
683
709
  this.incrementTerminationStat('incoming', reason);
684
710
  }
685
- cleanupOnce();
711
+ this.cleanupConnection(connectionRecord, reason);
686
712
  };
687
713
 
688
714
  // Helper to reject an incoming connection
@@ -691,9 +717,10 @@ export class PortProxy {
691
717
  socket.end();
692
718
  if (incomingTerminationReason === null) {
693
719
  incomingTerminationReason = reason;
720
+ connectionRecord.incomingTerminationReason = reason;
694
721
  this.incrementTerminationStat('incoming', reason);
695
722
  }
696
- cleanupOnce();
723
+ this.cleanupConnection(connectionRecord, reason);
697
724
  };
698
725
 
699
726
  // Set an initial timeout for SNI data if needed
@@ -706,10 +733,11 @@ export class PortProxy {
706
733
  );
707
734
  if (incomingTerminationReason === null) {
708
735
  incomingTerminationReason = 'initial_timeout';
736
+ connectionRecord.incomingTerminationReason = 'initial_timeout';
709
737
  this.incrementTerminationStat('incoming', 'initial_timeout');
710
738
  }
711
739
  socket.end();
712
- cleanupOnce();
740
+ this.cleanupConnection(connectionRecord, 'initial_timeout');
713
741
  }
714
742
  }, this.settings.initialDataTimeout!);
715
743
 
@@ -783,9 +811,11 @@ export class PortProxy {
783
811
 
784
812
  if (side === 'incoming' && incomingTerminationReason === null) {
785
813
  incomingTerminationReason = reason;
814
+ connectionRecord.incomingTerminationReason = reason;
786
815
  this.incrementTerminationStat('incoming', reason);
787
816
  } else if (side === 'outgoing' && outgoingTerminationReason === null) {
788
817
  outgoingTerminationReason = reason;
818
+ connectionRecord.outgoingTerminationReason = reason;
789
819
  this.incrementTerminationStat('outgoing', reason);
790
820
  }
791
821
 
@@ -799,9 +829,11 @@ export class PortProxy {
799
829
 
800
830
  if (side === 'incoming' && incomingTerminationReason === null) {
801
831
  incomingTerminationReason = 'normal';
832
+ connectionRecord.incomingTerminationReason = 'normal';
802
833
  this.incrementTerminationStat('incoming', 'normal');
803
834
  } else if (side === 'outgoing' && outgoingTerminationReason === null) {
804
835
  outgoingTerminationReason = 'normal';
836
+ connectionRecord.outgoingTerminationReason = 'normal';
805
837
  this.incrementTerminationStat('outgoing', 'normal');
806
838
  // Record the time when outgoing socket closed.
807
839
  connectionRecord.outgoingClosedTime = Date.now();
@@ -956,21 +988,26 @@ export class PortProxy {
956
988
 
957
989
  // Apply socket optimizations
958
990
  targetSocket.setNoDelay(this.settings.noDelay);
991
+
992
+ // Apply keep-alive settings to the outgoing connection as well
959
993
  if (this.settings.keepAlive) {
960
994
  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);
995
+
996
+ // Apply enhanced TCP keep-alive options if enabled
997
+ if (this.settings.enableKeepAliveProbes) {
998
+ try {
999
+ if ('setKeepAliveProbes' in targetSocket) {
1000
+ (targetSocket as any).setKeepAliveProbes(10);
1001
+ }
1002
+ if ('setKeepAliveInterval' in targetSocket) {
1003
+ (targetSocket as any).setKeepAliveInterval(1000);
1004
+ }
1005
+ } catch (err) {
1006
+ // Ignore errors - these are optional enhancements
1007
+ if (this.settings.enableDetailedLogging) {
1008
+ console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
1009
+ }
971
1010
  }
972
- } catch (err) {
973
- // Ignore errors - these are optional enhancements
974
1011
  }
975
1012
  }
976
1013
 
@@ -1009,6 +1046,7 @@ export class PortProxy {
1009
1046
 
1010
1047
  if (outgoingTerminationReason === null) {
1011
1048
  outgoingTerminationReason = 'connection_failed';
1049
+ connectionRecord.outgoingTerminationReason = 'connection_failed';
1012
1050
  this.incrementTerminationStat('outgoing', 'connection_failed');
1013
1051
  }
1014
1052
 
@@ -1020,8 +1058,20 @@ export class PortProxy {
1020
1058
  targetSocket.on('close', handleClose('outgoing'));
1021
1059
  socket.on('close', handleClose('incoming'));
1022
1060
 
1023
- // Handle timeouts
1061
+ // Handle timeouts with keep-alive awareness
1024
1062
  socket.on('timeout', () => {
1063
+ // For keep-alive connections, just log a warning instead of closing
1064
+ if (connectionRecord.hasKeepAlive) {
1065
+ console.log(
1066
+ `[${connectionId}] Timeout event on incoming keep-alive connection from ${remoteIP} after ${plugins.prettyMs(
1067
+ this.settings.socketTimeout || 3600000
1068
+ )}. Connection preserved.`
1069
+ );
1070
+ // Don't close the connection - just log
1071
+ return;
1072
+ }
1073
+
1074
+ // For non-keep-alive connections, proceed with normal cleanup
1025
1075
  console.log(
1026
1076
  `[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(
1027
1077
  this.settings.socketTimeout || 3600000
@@ -1029,12 +1079,25 @@ export class PortProxy {
1029
1079
  );
1030
1080
  if (incomingTerminationReason === null) {
1031
1081
  incomingTerminationReason = 'timeout';
1082
+ connectionRecord.incomingTerminationReason = 'timeout';
1032
1083
  this.incrementTerminationStat('incoming', 'timeout');
1033
1084
  }
1034
1085
  initiateCleanupOnce('timeout_incoming');
1035
1086
  });
1036
1087
 
1037
1088
  targetSocket.on('timeout', () => {
1089
+ // For keep-alive connections, just log a warning instead of closing
1090
+ if (connectionRecord.hasKeepAlive) {
1091
+ console.log(
1092
+ `[${connectionId}] Timeout event on outgoing keep-alive connection from ${remoteIP} after ${plugins.prettyMs(
1093
+ this.settings.socketTimeout || 3600000
1094
+ )}. Connection preserved.`
1095
+ );
1096
+ // Don't close the connection - just log
1097
+ return;
1098
+ }
1099
+
1100
+ // For non-keep-alive connections, proceed with normal cleanup
1038
1101
  console.log(
1039
1102
  `[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(
1040
1103
  this.settings.socketTimeout || 3600000
@@ -1042,14 +1105,26 @@ export class PortProxy {
1042
1105
  );
1043
1106
  if (outgoingTerminationReason === null) {
1044
1107
  outgoingTerminationReason = 'timeout';
1108
+ connectionRecord.outgoingTerminationReason = 'timeout';
1045
1109
  this.incrementTerminationStat('outgoing', 'timeout');
1046
1110
  }
1047
1111
  initiateCleanupOnce('timeout_outgoing');
1048
1112
  });
1049
1113
 
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));
1114
+ // Set appropriate timeouts, or disable for immortal keep-alive connections
1115
+ if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
1116
+ // Disable timeouts completely for immortal connections
1117
+ socket.setTimeout(0);
1118
+ targetSocket.setTimeout(0);
1119
+
1120
+ if (this.settings.enableDetailedLogging) {
1121
+ console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
1122
+ }
1123
+ } else {
1124
+ // Set normal timeouts for other connections
1125
+ socket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
1126
+ targetSocket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
1127
+ }
1053
1128
 
1054
1129
  // Track outgoing data for bytes counting
1055
1130
  targetSocket.on('data', (chunk: Buffer) => {
@@ -1094,7 +1169,7 @@ export class PortProxy {
1094
1169
  ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
1095
1170
  : ''
1096
1171
  }` +
1097
- ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
1172
+ ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Yes' : 'No'}`
1098
1173
  );
1099
1174
  } else {
1100
1175
  console.log(
@@ -1125,7 +1200,7 @@ export class PortProxy {
1125
1200
  ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
1126
1201
  : ''
1127
1202
  }` +
1128
- ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
1203
+ ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Yes' : 'No'}`
1129
1204
  );
1130
1205
  } else {
1131
1206
  console.log(
@@ -1171,27 +1246,60 @@ export class PortProxy {
1171
1246
  });
1172
1247
  }
1173
1248
 
1174
- // Set connection timeout
1249
+ // Set connection timeout with simpler logic
1175
1250
  if (connectionRecord.cleanupTimer) {
1176
1251
  clearTimeout(connectionRecord.cleanupTimer);
1177
1252
  }
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
1253
 
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();
1254
+ // For immortal keep-alive connections, skip setting a timeout completely
1255
+ if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
1256
+ if (this.settings.enableDetailedLogging) {
1257
+ console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
1258
+ }
1259
+ // No cleanup timer for immortal connections
1260
+ }
1261
+ // For extended keep-alive connections, use extended timeout
1262
+ else if (connectionRecord.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
1263
+ const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
1264
+ const safeTimeout = ensureSafeTimeout(extendedTimeout);
1265
+
1266
+ connectionRecord.cleanupTimer = setTimeout(() => {
1267
+ console.log(
1268
+ `[${connectionId}] Keep-alive connection from ${remoteIP} exceeded extended lifetime (${plugins.prettyMs(
1269
+ extendedTimeout
1270
+ )}), forcing cleanup.`
1271
+ );
1272
+ initiateCleanupOnce('extended_lifetime');
1273
+ }, safeTimeout);
1274
+
1275
+ // Make sure timeout doesn't keep the process alive
1276
+ if (connectionRecord.cleanupTimer.unref) {
1277
+ connectionRecord.cleanupTimer.unref();
1278
+ }
1279
+
1280
+ if (this.settings.enableDetailedLogging) {
1281
+ console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
1282
+ }
1283
+ }
1284
+ // For standard connections, use normal timeout
1285
+ else {
1286
+ // Use domain-specific timeout if available, otherwise use default
1287
+ const connectionTimeout = connectionRecord.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
1288
+ const safeTimeout = ensureSafeTimeout(connectionTimeout);
1289
+
1290
+ connectionRecord.cleanupTimer = setTimeout(() => {
1291
+ console.log(
1292
+ `[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(
1293
+ connectionTimeout
1294
+ )}), forcing cleanup.`
1295
+ );
1296
+ initiateCleanupOnce('connection_timeout');
1297
+ }, safeTimeout);
1298
+
1299
+ // Make sure timeout doesn't keep the process alive
1300
+ if (connectionRecord.cleanupTimer.unref) {
1301
+ connectionRecord.cleanupTimer.unref();
1302
+ }
1195
1303
  }
1196
1304
 
1197
1305
  // Mark TLS handshake as complete for TLS connections
@@ -1385,6 +1493,7 @@ export class PortProxy {
1385
1493
  let nonTlsConnections = 0;
1386
1494
  let completedTlsHandshakes = 0;
1387
1495
  let pendingTlsHandshakes = 0;
1496
+ let keepAliveConnections = 0;
1388
1497
 
1389
1498
  // Create a copy of the keys to avoid modification during iteration
1390
1499
  const connectionIds = [...this.connectionRecords.keys()];
@@ -1404,6 +1513,10 @@ export class PortProxy {
1404
1513
  } else {
1405
1514
  nonTlsConnections++;
1406
1515
  }
1516
+
1517
+ if (record.hasKeepAlive) {
1518
+ keepAliveConnections++;
1519
+ }
1407
1520
 
1408
1521
  maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
1409
1522
  if (record.outgoingStartTime) {
@@ -1440,19 +1553,58 @@ export class PortProxy {
1440
1553
  );
1441
1554
  }
1442
1555
 
1443
- // Skip inactivity check if disabled
1444
- if (!this.settings.disableInactivityCheck) {
1445
- // Inactivity check with configurable timeout
1446
- const inactivityThreshold = this.settings.inactivityTimeout!;
1447
-
1556
+ // Skip inactivity check if disabled or for immortal keep-alive connections
1557
+ if (!this.settings.disableInactivityCheck &&
1558
+ !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
1559
+
1448
1560
  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');
1561
+
1562
+ // Use extended timeout for extended-treatment keep-alive connections
1563
+ let effectiveTimeout = this.settings.inactivityTimeout!;
1564
+ if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
1565
+ const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
1566
+ effectiveTimeout = effectiveTimeout * multiplier;
1567
+ }
1568
+
1569
+ if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
1570
+ // For keep-alive connections, issue a warning first
1571
+ if (record.hasKeepAlive && !record.inactivityWarningIssued) {
1572
+ console.log(
1573
+ `[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
1574
+ `Will close in 10 minutes if no activity.`
1575
+ );
1576
+
1577
+ // Set warning flag and add grace period
1578
+ record.inactivityWarningIssued = true;
1579
+ record.lastActivity = now - (effectiveTimeout - 600000);
1580
+
1581
+ // Try to stimulate activity with a probe packet
1582
+ if (record.outgoing && !record.outgoing.destroyed) {
1583
+ try {
1584
+ record.outgoing.write(Buffer.alloc(0));
1585
+
1586
+ if (this.settings.enableDetailedLogging) {
1587
+ console.log(`[${id}] Sent probe packet to test keep-alive connection`);
1588
+ }
1589
+ } catch (err) {
1590
+ console.log(`[${id}] Error sending probe packet: ${err}`);
1591
+ }
1592
+ }
1593
+ } else {
1594
+ // For non-keep-alive or after warning, close the connection
1595
+ console.log(
1596
+ `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
1597
+ `for ${plugins.prettyMs(inactivityTime)}.` +
1598
+ (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
1599
+ );
1600
+ this.cleanupConnection(record, 'inactivity');
1601
+ }
1602
+ } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
1603
+ // If activity detected after warning, clear the warning
1604
+ if (this.settings.enableDetailedLogging) {
1605
+ console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`);
1606
+ }
1607
+ record.inactivityWarningIssued = false;
1456
1608
  }
1457
1609
  }
1458
1610
  }
@@ -1460,7 +1612,8 @@ export class PortProxy {
1460
1612
  // Log detailed stats periodically
1461
1613
  console.log(
1462
1614
  `Active connections: ${this.connectionRecords.size}. ` +
1463
- `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` +
1615
+ `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
1616
+ `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}. ` +
1464
1617
  `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
1465
1618
  maxOutgoing
1466
1619
  )}. ` +