@push.rocks/smartproxy 3.32.1 → 3.32.2

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,18 +10,13 @@ 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)
17
17
  }
18
18
 
19
- /**
20
- * Port proxy settings including global allowed port ranges
21
- *
22
- * NOTE: In version 3.31.0+, timeout settings have been simplified and hardcoded with sensible defaults
23
- * to ensure TLS certificate safety in all deployment scenarios, especially chained proxies.
24
- */
19
+ /** Port proxy settings including global allowed port ranges */
25
20
  export interface IPortProxySettings extends plugins.tls.TlsOptions {
26
21
  fromPort: number;
27
22
  toPort: number;
@@ -32,10 +27,14 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
32
27
  defaultBlockedIPs?: string[];
33
28
  preserveSourceIP?: boolean;
34
29
 
35
- // Simplified timeout settings
30
+ // Timeout settings
31
+ initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
32
+ socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
33
+ inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
34
+ maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h)
35
+ inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
36
+
36
37
  gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
37
-
38
- // Ranged port settings
39
38
  globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
40
39
  forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
41
40
 
@@ -45,7 +44,9 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
45
44
  keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
46
45
  maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
47
46
 
48
- // Logging settings
47
+ // Enhanced features
48
+ disableInactivityCheck?: boolean; // Disable inactivity checking entirely
49
+ enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
49
50
  enableDetailedLogging?: boolean; // Enable detailed connection logging
50
51
  enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
51
52
  enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
@@ -53,22 +54,14 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
53
54
  // Rate limiting and security
54
55
  maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
55
56
  connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
56
-
57
- // NetworkProxy integration
58
- networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
59
57
 
60
- // New settings for chained proxy configurations and TLS handling
61
- isChainedProxy?: boolean; // Whether this proxy is part of a proxy chain (detected automatically if unspecified)
62
- chainPosition?: 'first' | 'middle' | 'last'; // Position in the proxy chain (affects TLS handling)
63
- aggressiveTlsRefresh?: boolean; // Use more aggressive TLS refresh timeouts (default: true for chained)
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)
64
62
 
65
- // TLS session cache configuration
66
- tlsSessionCache?: {
67
- enabled?: boolean; // Whether to use the TLS session cache (default: true)
68
- maxEntries?: number; // Maximum cache entries (default: 10000)
69
- expiryTime?: number; // Session expiry time in ms (default: 24h)
70
- cleanupInterval?: number; // Cache cleanup interval in ms (default: 10min)
71
- };
63
+ // New property for NetworkProxy integration
64
+ networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
72
65
  }
73
66
 
74
67
  /**
@@ -97,810 +90,182 @@ interface IConnectionRecord {
97
90
  tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
98
91
  hasReceivedInitialData: boolean; // Whether initial data has been received
99
92
  domainConfig?: IDomainConfig; // Associated domain config for this connection
100
-
93
+
101
94
  // Keep-alive tracking
102
95
  hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
103
- incomingKeepAliveEnabled?: boolean; // Whether keep-alive is enabled on incoming socket
104
- outgoingKeepAliveEnabled?: boolean; // Whether keep-alive is enabled on outgoing socket
105
96
  inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
106
97
  incomingTerminationReason?: string | null; // Reason for incoming termination
107
98
  outgoingTerminationReason?: string | null; // Reason for outgoing termination
108
-
99
+
109
100
  // New field for NetworkProxy tracking
110
101
  usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
111
102
  networkProxyIndex?: number; // Which NetworkProxy instance is being used
112
-
113
- // Sleep detection fields
114
- possibleSystemSleep?: boolean; // Flag to indicate a possible system sleep was detected
115
- lastSleepDetection?: number; // Timestamp of the last sleep detection
116
- }
117
-
118
- /**
119
- * Structure to track TLS session information for proper resumption handling
120
- */
121
- interface ITlsSessionInfo {
122
- domain: string; // The SNI domain associated with this session
123
- sessionId?: Buffer; // The TLS session ID (if available)
124
- ticketId?: string; // Session ticket identifier for newer TLS versions
125
- ticketTimestamp: number; // When this session was recorded
126
- lastAccessed?: number; // When this session was last accessed
127
- accessCount?: number; // How many times this session has been used
128
- }
129
-
130
- /**
131
- * Configuration for TLS session cache
132
- */
133
- interface ITlsSessionCacheConfig {
134
- maxEntries: number; // Maximum number of entries to keep in the cache
135
- expiryTime: number; // Time in ms before sessions expire (default: 24 hours)
136
- cleanupInterval: number; // Interval in ms to run cleanup (default: 10 minutes)
137
- enabled: boolean; // Whether session caching is enabled
138
- }
139
-
140
- // Default configuration for session cache with relaxed timeouts
141
- const DEFAULT_SESSION_CACHE_CONFIG: ITlsSessionCacheConfig = {
142
- maxEntries: 20000, // Default max 20,000 entries (doubled)
143
- expiryTime: 7 * 24 * 60 * 60 * 1000, // 7 days default (increased from 24 hours)
144
- cleanupInterval: 30 * 60 * 1000, // Clean up every 30 minutes (relaxed from 10 minutes)
145
- enabled: true // Enabled by default
146
- };
147
-
148
- // Enhanced TLS session cache with size limits and better performance
149
- class TlsSessionCache {
150
- private cache = new Map<string, ITlsSessionInfo>();
151
- private config: ITlsSessionCacheConfig;
152
- private cleanupTimer: NodeJS.Timeout | null = null;
153
- private lastCleanupTime: number = 0;
154
- private cacheStats = {
155
- hits: 0,
156
- misses: 0,
157
- expirations: 0,
158
- evictions: 0,
159
- total: 0
160
- };
161
-
162
- constructor(config?: Partial<ITlsSessionCacheConfig>) {
163
- this.config = { ...DEFAULT_SESSION_CACHE_CONFIG, ...config };
164
- this.startCleanupTimer();
165
- }
166
-
167
- /**
168
- * Get a session from the cache
169
- */
170
- public get(key: string): ITlsSessionInfo | undefined {
171
- // Skip if cache is disabled
172
- if (!this.config.enabled) return undefined;
173
-
174
- const entry = this.cache.get(key);
175
-
176
- if (entry) {
177
- // Update access information
178
- entry.lastAccessed = Date.now();
179
- entry.accessCount = (entry.accessCount || 0) + 1;
180
- this.cache.set(key, entry);
181
- this.cacheStats.hits++;
182
- return entry;
183
- }
184
-
185
- this.cacheStats.misses++;
186
- return undefined;
187
- }
188
-
189
- /**
190
- * Check if the cache has a key
191
- */
192
- public has(key: string): boolean {
193
- // Skip if cache is disabled
194
- if (!this.config.enabled) return false;
195
-
196
- const exists = this.cache.has(key);
197
- if (exists) {
198
- const entry = this.cache.get(key)!;
199
-
200
- // Check if entry has expired
201
- if (Date.now() - entry.ticketTimestamp > this.config.expiryTime) {
202
- this.cache.delete(key);
203
- this.cacheStats.expirations++;
204
- return false;
205
- }
206
-
207
- // Update last accessed time
208
- entry.lastAccessed = Date.now();
209
- this.cache.set(key, entry);
210
- }
211
-
212
- return exists;
213
- }
214
-
215
- /**
216
- * Set a session in the cache
217
- */
218
- public set(key: string, value: ITlsSessionInfo): void {
219
- // Skip if cache is disabled
220
- if (!this.config.enabled) return;
221
-
222
- // Ensure timestamps are set
223
- const entry = {
224
- ...value,
225
- lastAccessed: Date.now(),
226
- accessCount: 0
227
- };
228
-
229
- // Check if we need to evict entries
230
- if (!this.cache.has(key) && this.cache.size >= this.config.maxEntries) {
231
- this.evictOldest();
232
- }
233
-
234
- this.cache.set(key, entry);
235
- this.cacheStats.total = this.cache.size;
236
-
237
- // Run cleanup if it's been a while
238
- const timeSinceCleanup = Date.now() - this.lastCleanupTime;
239
- if (timeSinceCleanup > this.config.cleanupInterval * 2) {
240
- this.cleanup();
241
- }
242
- }
243
-
244
- /**
245
- * Delete a session from the cache
246
- */
247
- public delete(key: string): boolean {
248
- return this.cache.delete(key);
249
- }
250
-
251
- /**
252
- * Clear the entire cache
253
- */
254
- public clear(): void {
255
- this.cache.clear();
256
- this.cacheStats.total = 0;
257
- }
258
-
259
- /**
260
- * Get cache statistics
261
- */
262
- public getStats(): any {
263
- return {
264
- ...this.cacheStats,
265
- size: this.cache.size,
266
- enabled: this.config.enabled,
267
- maxEntries: this.config.maxEntries,
268
- expiryTimeHours: this.config.expiryTime / (60 * 60 * 1000)
269
- };
270
- }
271
-
272
- /**
273
- * Update cache configuration
274
- */
275
- public updateConfig(config: Partial<ITlsSessionCacheConfig>): void {
276
- this.config = { ...this.config, ...config };
277
-
278
- // Restart the cleanup timer with new interval
279
- this.startCleanupTimer();
280
-
281
- // Run immediate cleanup if max entries was reduced
282
- if (config.maxEntries && this.cache.size > config.maxEntries) {
283
- while (this.cache.size > config.maxEntries) {
284
- this.evictOldest();
285
- }
286
- }
287
- }
288
-
289
- /**
290
- * Start the cleanup timer
291
- */
292
- private startCleanupTimer(): void {
293
- if (this.cleanupTimer) {
294
- clearInterval(this.cleanupTimer);
295
- this.cleanupTimer = null;
296
- }
297
-
298
- if (!this.config.enabled) return;
299
-
300
- this.cleanupTimer = setInterval(() => {
301
- this.cleanup();
302
- }, this.config.cleanupInterval);
303
-
304
- // Make sure the interval doesn't keep the process alive
305
- if (this.cleanupTimer.unref) {
306
- this.cleanupTimer.unref();
307
- }
308
- }
309
-
310
- /**
311
- * Clean up expired entries
312
- */
313
- private cleanup(): void {
314
- this.lastCleanupTime = Date.now();
315
-
316
- const now = Date.now();
317
- let expiredCount = 0;
318
-
319
- for (const [key, info] of this.cache.entries()) {
320
- if (now - info.ticketTimestamp > this.config.expiryTime) {
321
- this.cache.delete(key);
322
- expiredCount++;
323
- }
324
- }
325
-
326
- if (expiredCount > 0) {
327
- this.cacheStats.expirations += expiredCount;
328
- this.cacheStats.total = this.cache.size;
329
- console.log(`TLS Session Cache: Cleaned up ${expiredCount} expired entries. ${this.cache.size} entries remaining.`);
330
- }
331
- }
332
-
333
- /**
334
- * Evict the oldest entries when cache is full
335
- */
336
- private evictOldest(): void {
337
- if (this.cache.size === 0) return;
338
-
339
- let oldestKey: string | null = null;
340
- let oldestTime = Date.now();
341
-
342
- // Strategy: Find least recently accessed entry
343
- for (const [key, info] of this.cache.entries()) {
344
- const lastAccess = info.lastAccessed || info.ticketTimestamp;
345
- if (lastAccess < oldestTime) {
346
- oldestTime = lastAccess;
347
- oldestKey = key;
348
- }
349
- }
350
-
351
- if (oldestKey) {
352
- this.cache.delete(oldestKey);
353
- this.cacheStats.evictions++;
354
- }
355
- }
356
-
357
- /**
358
- * Stop cleanup timer (used during shutdown)
359
- */
360
- public stop(): void {
361
- if (this.cleanupTimer) {
362
- clearInterval(this.cleanupTimer);
363
- this.cleanupTimer = null;
364
- }
365
- }
366
- }
367
-
368
- // Create the global session cache
369
- const tlsSessionCache = new TlsSessionCache();
370
-
371
- // Legacy function for backward compatibility
372
- function stopSessionCleanupTimer() {
373
- tlsSessionCache.stop();
374
- }
375
-
376
- /**
377
- * Return type for the extractSNIInfo function
378
- */
379
- interface ISNIExtractResult {
380
- serverName?: string; // The extracted SNI hostname
381
- sessionId?: Buffer; // The TLS session ID if present
382
- sessionIdKey?: string; // The hex string representation of session ID
383
- sessionTicketId?: string; // Session ticket identifier for TLS 1.3+ resumption
384
- hasSessionTicket?: boolean; // Whether a session ticket extension was found
385
- isResumption: boolean; // Whether this appears to be a session resumption
386
- resumedDomain?: string; // The domain associated with the session if resuming
387
- partialExtract?: boolean; // Whether this was only a partial extraction (more data needed)
388
- recordsExamined?: number; // Number of TLS records examined in the buffer
389
- multipleRecords?: boolean; // Whether multiple TLS records were found in the buffer
390
103
  }
391
104
 
392
105
  /**
393
106
  * Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
394
107
  * Enhanced for robustness and detailed logging.
395
- * Also extracts and tracks TLS Session IDs for session resumption handling.
396
- *
397
- * Improved to handle:
398
- * - Multiple TLS records in a single buffer
399
- * - Fragmented TLS handshakes across multiple records
400
- * - Partial TLS records that may continue in future chunks
401
- *
402
108
  * @param buffer - Buffer containing the TLS ClientHello.
403
109
  * @param enableLogging - Whether to enable detailed logging.
404
- * @returns An object containing SNI and session information, or undefined if parsing fails.
110
+ * @returns The server name if found, otherwise undefined.
405
111
  */
406
- function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
112
+ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
407
113
  try {
408
114
  // Check if buffer is too small for TLS
409
115
  if (buffer.length < 5) {
410
116
  if (enableLogging) console.log('Buffer too small for TLS header');
411
- return {
412
- isResumption: false,
413
- partialExtract: true // Indicating we need more data
414
- };
117
+ return undefined;
415
118
  }
416
119
 
417
- // Check first record type (has to be handshake - 22)
120
+ // Check record type (has to be handshake - 22)
418
121
  const recordType = buffer.readUInt8(0);
419
122
  if (recordType !== 22) {
420
123
  if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
421
124
  return undefined;
422
125
  }
423
126
 
424
- // Track multiple records and total records examined
425
- let recordsExamined = 0;
426
- let multipleRecords = false;
427
- let currentPosition = 0;
428
- let result: ISNIExtractResult | undefined;
127
+ // Check TLS version (has to be 3.1 or higher)
128
+ const majorVersion = buffer.readUInt8(1);
129
+ const minorVersion = buffer.readUInt8(2);
130
+ if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
429
131
 
430
- // Process potentially multiple TLS records in the buffer
431
- while (currentPosition + 5 <= buffer.length) {
432
- recordsExamined++;
433
-
434
- // Read record header
435
- const currentRecordType = buffer.readUInt8(currentPosition);
436
-
437
- // Only process handshake records (type 22)
438
- if (currentRecordType !== 22) {
439
- if (enableLogging) console.log(`Skipping non-handshake record at position ${currentPosition}, type: ${currentRecordType}`);
440
-
441
- // Move to next potential record
442
- if (currentPosition + 5 <= buffer.length) {
443
- // Need at least 5 bytes to determine next record length
444
- const nextRecordLength = buffer.readUInt16BE(currentPosition + 3);
445
- currentPosition += 5 + nextRecordLength;
446
- multipleRecords = true;
447
- continue;
448
- } else {
449
- // Not enough data to determine next record
450
- break;
451
- }
452
- }
453
-
454
- // Check TLS version
455
- const majorVersion = buffer.readUInt8(currentPosition + 1);
456
- const minorVersion = buffer.readUInt8(currentPosition + 2);
457
- if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion} at position ${currentPosition}`);
458
-
459
- // Get record length
460
- const recordLength = buffer.readUInt16BE(currentPosition + 3);
461
-
462
- // Check if we have the complete record
463
- if (currentPosition + 5 + recordLength > buffer.length) {
464
- if (enableLogging) {
465
- console.log(`Incomplete TLS record at position ${currentPosition}. Expected: ${currentPosition + 5 + recordLength}, Got: ${buffer.length}`);
466
- }
467
-
468
- // Return partial info and signal that more data is needed
469
- return {
470
- isResumption: false,
471
- partialExtract: true,
472
- recordsExamined,
473
- multipleRecords
474
- };
475
- }
476
-
477
- // Process this record - extract handshake information
478
- const recordResult = extractSNIFromRecord(
479
- buffer.slice(currentPosition, currentPosition + 5 + recordLength),
480
- enableLogging
481
- );
482
-
483
- // If we found SNI or session info in this record, store it
484
- if (recordResult && (recordResult.serverName || recordResult.isResumption)) {
485
- result = recordResult;
486
- result.recordsExamined = recordsExamined;
487
- result.multipleRecords = multipleRecords;
488
-
489
- // Once we've found SNI or session resumption info, we can stop processing
490
- // But we'll still set the multipleRecords flag to indicate more records exist
491
- if (currentPosition + 5 + recordLength < buffer.length) {
492
- result.multipleRecords = true;
493
- }
494
-
495
- break;
496
- }
497
-
498
- // Move to the next record
499
- currentPosition += 5 + recordLength;
500
-
501
- // Set the flag if we've processed multiple records
502
- if (currentPosition < buffer.length) {
503
- multipleRecords = true;
504
- }
505
- }
506
-
507
- // If we processed records but didn't find SNI or session info
508
- if (recordsExamined > 0 && !result) {
509
- return {
510
- isResumption: false,
511
- recordsExamined,
512
- multipleRecords
513
- };
132
+ // Check record length
133
+ const recordLength = buffer.readUInt16BE(3);
134
+ if (buffer.length < 5 + recordLength) {
135
+ if (enableLogging)
136
+ console.log(
137
+ `Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`
138
+ );
139
+ return undefined;
514
140
  }
515
-
516
- return result;
517
- } catch (err) {
518
- console.log(`Error extracting SNI: ${err}`);
519
- return undefined;
520
- }
521
- }
522
141
 
523
- /**
524
- * Extracts SNI information from a single TLS record
525
- * This helper function processes a single complete TLS record
526
- */
527
- function extractSNIFromRecord(recordBuffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
528
- try {
529
- // Skip the 5-byte TLS record header
530
142
  let offset = 5;
531
-
532
- // Verify this is a handshake message
533
- const handshakeType = recordBuffer.readUInt8(offset);
534
- if (handshakeType !== 1) { // 1 = ClientHello
143
+ const handshakeType = buffer.readUInt8(offset);
144
+ if (handshakeType !== 1) {
535
145
  if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
536
146
  return undefined;
537
147
  }
538
-
539
- // Skip the 4-byte handshake header (type + 3 bytes length)
540
- offset += 4;
541
-
542
- // Check if we have at least 38 more bytes for protocol version and random
543
- if (offset + 38 > recordBuffer.length) {
544
- if (enableLogging) console.log('Buffer too small for handshake header');
545
- return undefined;
546
- }
547
-
148
+
149
+ offset += 4; // Skip handshake header (type + length)
150
+
548
151
  // Client version
549
- const clientMajorVersion = recordBuffer.readUInt8(offset);
550
- const clientMinorVersion = recordBuffer.readUInt8(offset + 1);
152
+ const clientMajorVersion = buffer.readUInt8(offset);
153
+ const clientMinorVersion = buffer.readUInt8(offset + 1);
551
154
  if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
552
-
553
- // Skip version and random (2 + 32 bytes)
554
- offset += 2 + 32;
555
-
155
+
156
+ offset += 2 + 32; // Skip client version and random
157
+
556
158
  // Session ID
557
- if (offset + 1 > recordBuffer.length) {
558
- if (enableLogging) console.log('Buffer too small for session ID length');
559
- return undefined;
560
- }
561
-
562
- // Extract Session ID for session resumption tracking
563
- const sessionIDLength = recordBuffer.readUInt8(offset);
159
+ const sessionIDLength = buffer.readUInt8(offset);
564
160
  if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
565
-
566
- // If there's a session ID, extract it
567
- let sessionId: Buffer | undefined;
568
- let sessionIdKey: string | undefined;
569
- let isResumption = false;
570
- let resumedDomain: string | undefined;
571
-
572
- if (sessionIDLength > 0) {
573
- if (offset + 1 + sessionIDLength > recordBuffer.length) {
574
- if (enableLogging) console.log('Buffer too small for session ID data');
575
- return undefined;
576
- }
577
-
578
- sessionId = Buffer.from(recordBuffer.slice(offset + 1, offset + 1 + sessionIDLength));
579
-
580
- // Convert sessionId to a string key for our cache
581
- sessionIdKey = sessionId.toString('hex');
582
-
583
- if (enableLogging) {
584
- console.log(`Session ID: ${sessionIdKey}`);
585
- }
586
-
587
- // Check if this is a session resumption attempt
588
- if (tlsSessionCache.has(sessionIdKey)) {
589
- const cachedInfo = tlsSessionCache.get(sessionIdKey)!;
590
- resumedDomain = cachedInfo.domain;
591
- isResumption = true;
592
-
593
- if (enableLogging) {
594
- console.log(`TLS Session Resumption detected for domain: ${resumedDomain}`);
595
- }
596
- }
597
- }
598
-
599
- offset += 1 + sessionIDLength; // Skip session ID length and data
600
-
161
+ offset += 1 + sessionIDLength; // Skip session ID
162
+
601
163
  // Cipher suites
602
- if (offset + 2 > recordBuffer.length) {
164
+ if (offset + 2 > buffer.length) {
603
165
  if (enableLogging) console.log('Buffer too small for cipher suites length');
604
166
  return undefined;
605
167
  }
606
-
607
- const cipherSuitesLength = recordBuffer.readUInt16BE(offset);
168
+ const cipherSuitesLength = buffer.readUInt16BE(offset);
608
169
  if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
609
-
610
- if (offset + 2 + cipherSuitesLength > recordBuffer.length) {
611
- if (enableLogging) console.log('Buffer too small for cipher suites data');
612
- return undefined;
613
- }
614
-
615
- offset += 2 + cipherSuitesLength; // Skip cipher suites length and data
616
-
170
+ offset += 2 + cipherSuitesLength; // Skip cipher suites
171
+
617
172
  // Compression methods
618
- if (offset + 1 > recordBuffer.length) {
173
+ if (offset + 1 > buffer.length) {
619
174
  if (enableLogging) console.log('Buffer too small for compression methods length');
620
175
  return undefined;
621
176
  }
622
-
623
- const compressionMethodsLength = recordBuffer.readUInt8(offset);
177
+ const compressionMethodsLength = buffer.readUInt8(offset);
624
178
  if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
625
-
626
- if (offset + 1 + compressionMethodsLength > recordBuffer.length) {
627
- if (enableLogging) console.log('Buffer too small for compression methods data');
179
+ offset += 1 + compressionMethodsLength; // Skip compression methods
180
+
181
+ // Extensions
182
+ if (offset + 2 > buffer.length) {
183
+ if (enableLogging) console.log('Buffer too small for extensions length');
628
184
  return undefined;
629
185
  }
630
-
631
- offset += 1 + compressionMethodsLength; // Skip compression methods length and data
632
-
633
- // Check if we have extensions data
634
- if (offset + 2 > recordBuffer.length) {
635
- if (enableLogging) console.log('No extensions data found - end of ClientHello');
636
-
637
- // Even without SNI, we might be dealing with a session resumption
638
- if (isResumption && resumedDomain) {
639
- return {
640
- serverName: resumedDomain, // Use the domain from previous session
641
- sessionId,
642
- sessionIdKey,
643
- hasSessionTicket: false,
644
- isResumption: true,
645
- resumedDomain
646
- };
647
- }
648
-
649
- return {
650
- isResumption,
651
- sessionId,
652
- sessionIdKey
653
- };
654
- }
655
-
656
- // Extensions
657
- const extensionsLength = recordBuffer.readUInt16BE(offset);
186
+ const extensionsLength = buffer.readUInt16BE(offset);
658
187
  if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
659
-
660
188
  offset += 2;
661
189
  const extensionsEnd = offset + extensionsLength;
662
-
663
- if (extensionsEnd > recordBuffer.length) {
664
- if (enableLogging) {
665
- console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${recordBuffer.length}`);
666
- }
667
-
668
- // Even without complete extensions, we might be dealing with a session resumption
669
- if (isResumption && resumedDomain) {
670
- return {
671
- serverName: resumedDomain, // Use the domain from previous session
672
- sessionId,
673
- sessionIdKey,
674
- hasSessionTicket: false,
675
- isResumption: true,
676
- resumedDomain
677
- };
678
- }
679
-
680
- return {
681
- isResumption,
682
- sessionId,
683
- sessionIdKey,
684
- partialExtract: true // Indicating we have incomplete extensions data
685
- };
190
+
191
+ if (extensionsEnd > buffer.length) {
192
+ if (enableLogging)
193
+ console.log(
194
+ `Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`
195
+ );
196
+ return undefined;
686
197
  }
687
-
688
- // Variables to track session tickets
689
- let hasSessionTicket = false;
690
- let sessionTicketId: string | undefined;
691
-
198
+
692
199
  // Parse extensions
693
200
  while (offset + 4 <= extensionsEnd) {
694
- const extensionType = recordBuffer.readUInt16BE(offset);
695
- const extensionLength = recordBuffer.readUInt16BE(offset + 2);
696
-
697
- if (enableLogging) {
201
+ const extensionType = buffer.readUInt16BE(offset);
202
+ const extensionLength = buffer.readUInt16BE(offset + 2);
203
+
204
+ if (enableLogging)
698
205
  console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
699
- }
700
-
701
- if (offset + 4 + extensionLength > recordBuffer.length) {
702
- if (enableLogging) {
703
- console.log(`Extension data incomplete. Expected: ${offset + 4 + extensionLength}, Got: ${recordBuffer.length}`);
704
- }
705
- return {
706
- isResumption,
707
- sessionId,
708
- sessionIdKey,
709
- hasSessionTicket,
710
- partialExtract: true
711
- };
712
- }
713
-
206
+
714
207
  offset += 4;
715
-
716
- // Check for Session Ticket extension (type 0x0023)
717
- if (extensionType === 0x0023 && extensionLength > 0) {
718
- hasSessionTicket = true;
719
-
720
- // Extract a hash of the ticket for tracking
721
- if (extensionLength > 16) { // Ensure we have enough bytes to create a meaningful ID
722
- const ticketBytes = recordBuffer.slice(offset, offset + Math.min(16, extensionLength));
723
- sessionTicketId = ticketBytes.toString('hex');
724
-
725
- if (enableLogging) {
726
- console.log(`Session Ticket found, ID: ${sessionTicketId}`);
727
-
728
- // Check if this is a known session ticket
729
- if (tlsSessionCache.has(`ticket:${sessionTicketId}`)) {
730
- const cachedInfo = tlsSessionCache.get(`ticket:${sessionTicketId}`);
731
- console.log(`TLS Session Ticket Resumption detected for domain: ${cachedInfo?.domain}`);
732
-
733
- // Set isResumption and resumedDomain if not already set
734
- if (!isResumption && !resumedDomain) {
735
- isResumption = true;
736
- resumedDomain = cachedInfo?.domain;
737
- }
738
- }
739
- }
740
- }
741
- }
742
-
743
- // Server Name Indication extension (type 0x0000)
208
+
744
209
  if (extensionType === 0x0000) {
745
- if (offset + 2 > recordBuffer.length) {
210
+ // SNI extension
211
+ if (offset + 2 > buffer.length) {
746
212
  if (enableLogging) console.log('Buffer too small for SNI list length');
747
- return {
748
- isResumption,
749
- sessionId,
750
- sessionIdKey,
751
- hasSessionTicket,
752
- partialExtract: true
753
- };
213
+ return undefined;
754
214
  }
755
-
756
- const sniListLength = recordBuffer.readUInt16BE(offset);
215
+
216
+ const sniListLength = buffer.readUInt16BE(offset);
757
217
  if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
758
-
759
218
  offset += 2;
760
219
  const sniListEnd = offset + sniListLength;
761
-
762
- if (sniListEnd > recordBuffer.length) {
763
- if (enableLogging) {
764
- console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${recordBuffer.length}`);
765
- }
766
- return {
767
- isResumption,
768
- sessionId,
769
- sessionIdKey,
770
- hasSessionTicket,
771
- partialExtract: true
772
- };
220
+
221
+ if (sniListEnd > buffer.length) {
222
+ if (enableLogging)
223
+ console.log(
224
+ `Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`
225
+ );
226
+ return undefined;
773
227
  }
774
-
228
+
775
229
  while (offset + 3 < sniListEnd) {
776
- const nameType = recordBuffer.readUInt8(offset++);
777
-
778
- if (offset + 2 > recordBuffer.length) {
779
- if (enableLogging) console.log('Buffer too small for SNI name length');
780
- return {
781
- isResumption,
782
- sessionId,
783
- sessionIdKey,
784
- hasSessionTicket,
785
- partialExtract: true
786
- };
787
- }
788
-
789
- const nameLen = recordBuffer.readUInt16BE(offset);
230
+ const nameType = buffer.readUInt8(offset++);
231
+ const nameLen = buffer.readUInt16BE(offset);
790
232
  offset += 2;
791
-
233
+
792
234
  if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
793
-
794
- // Only process hostname entries (type 0)
235
+
795
236
  if (nameType === 0) {
796
- if (offset + nameLen > recordBuffer.length) {
797
- if (enableLogging) {
798
- console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${recordBuffer.length}`);
799
- }
800
- return {
801
- isResumption,
802
- sessionId,
803
- sessionIdKey,
804
- hasSessionTicket,
805
- partialExtract: true
806
- };
237
+ // host_name
238
+ if (offset + nameLen > buffer.length) {
239
+ if (enableLogging)
240
+ console.log(
241
+ `Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${
242
+ buffer.length
243
+ }`
244
+ );
245
+ return undefined;
807
246
  }
808
-
809
- const serverName = recordBuffer.toString('utf8', offset, offset + nameLen);
247
+
248
+ const serverName = buffer.toString('utf8', offset, offset + nameLen);
810
249
  if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
811
-
812
- // Store the session ID to domain mapping for future resumptions
813
- if (sessionIdKey && sessionId && serverName) {
814
- tlsSessionCache.set(sessionIdKey, {
815
- domain: serverName,
816
- sessionId: sessionId,
817
- ticketTimestamp: Date.now()
818
- });
819
-
820
- if (enableLogging) {
821
- console.log(`Stored session ${sessionIdKey} for domain ${serverName}`);
822
- }
823
- }
824
-
825
- // Also store session ticket information if present
826
- if (sessionTicketId && serverName) {
827
- tlsSessionCache.set(`ticket:${sessionTicketId}`, {
828
- domain: serverName,
829
- ticketId: sessionTicketId,
830
- ticketTimestamp: Date.now()
831
- });
832
-
833
- if (enableLogging) {
834
- console.log(`Stored session ticket ${sessionTicketId} for domain ${serverName}`);
835
- }
836
- }
837
-
838
- // Return the complete extraction result
839
- return {
840
- serverName,
841
- sessionId,
842
- sessionIdKey,
843
- sessionTicketId,
844
- isResumption,
845
- resumedDomain,
846
- hasSessionTicket
847
- };
250
+ return serverName;
848
251
  }
849
-
850
- // Skip this name entry
252
+
851
253
  offset += nameLen;
852
254
  }
853
-
854
- // Finished processing the SNI extension without finding a hostname
855
255
  break;
856
256
  } else {
857
- // Skip other extensions
858
257
  offset += extensionLength;
859
258
  }
860
259
  }
861
-
862
- // We finished processing all extensions without finding SNI
260
+
863
261
  if (enableLogging) console.log('No SNI extension found');
864
-
865
- // Even without SNI, we might be dealing with a session resumption
866
- if (isResumption && resumedDomain) {
867
- return {
868
- serverName: resumedDomain, // Use the domain from previous session
869
- sessionId,
870
- sessionIdKey,
871
- sessionTicketId,
872
- hasSessionTicket,
873
- isResumption: true,
874
- resumedDomain
875
- };
876
- }
877
-
878
- // Return a basic result with just the session info
879
- return {
880
- isResumption,
881
- sessionId,
882
- sessionIdKey,
883
- sessionTicketId,
884
- hasSessionTicket,
885
- resumedDomain
886
- };
262
+ return undefined;
887
263
  } catch (err) {
888
- console.log(`Error in extractSNIFromRecord: ${err}`);
264
+ console.log(`Error extracting SNI: ${err}`);
889
265
  return undefined;
890
266
  }
891
267
  }
892
268
 
893
- /**
894
- * Legacy wrapper for extractSNIInfo to maintain backward compatibility
895
- * @param buffer - Buffer containing the TLS ClientHello
896
- * @param enableLogging - Whether to enable detailed logging
897
- * @returns The server name if found, otherwise undefined
898
- */
899
- function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
900
- const result = extractSNIInfo(buffer, enableLogging);
901
- return result?.serverName;
902
- }
903
-
904
269
  // Helper: Check if a port falls within any of the given port ranges
905
270
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
906
271
  return ranges.some((range) => port >= range.from && port <= range.to);
@@ -963,22 +328,7 @@ const randomizeTimeout = (baseTimeout: number, variationPercent: number = 5): nu
963
328
 
964
329
  export class PortProxy {
965
330
  private netServers: plugins.net.Server[] = [];
966
-
967
- // Define the internal settings interface to include all fields, including those removed from the public interface
968
- settings: IPortProxySettings & {
969
- // Internal fields removed from public interface in 3.31.0+
970
- initialDataTimeout: number;
971
- socketTimeout: number;
972
- inactivityCheckInterval: number;
973
- maxConnectionLifetime: number;
974
- inactivityTimeout: number;
975
- disableInactivityCheck: boolean;
976
- enableKeepAliveProbes: boolean;
977
- keepAliveTreatment: 'standard' | 'extended' | 'immortal';
978
- keepAliveInactivityMultiplier: number;
979
- extendedKeepAliveLifetime: number;
980
- };
981
-
331
+ settings: IPortProxySettings;
982
332
  private connectionRecords: Map<string, IConnectionRecord> = new Map();
983
333
  private connectionLogger: NodeJS.Timeout | null = null;
984
334
  private isShuttingDown: boolean = false;
@@ -998,116 +348,51 @@ export class PortProxy {
998
348
  // Connection tracking by IP for rate limiting
999
349
  private connectionsByIP: Map<string, Set<string>> = new Map();
1000
350
  private connectionRateByIP: Map<string, number[]> = new Map();
1001
-
351
+
1002
352
  // New property to store NetworkProxy instances
1003
353
  private networkProxies: NetworkProxy[] = [];
1004
354
 
1005
355
  constructor(settingsArg: IPortProxySettings) {
1006
- // Auto-detect if this is a chained proxy based on targetIP
1007
- const targetIP = settingsArg.targetIP || 'localhost';
1008
- const isChainedProxy = settingsArg.isChainedProxy !== undefined
1009
- ? settingsArg.isChainedProxy
1010
- : (targetIP === 'localhost' || targetIP === '127.0.0.1');
1011
-
1012
- // Use more aggressive timeouts for chained proxies
1013
- const aggressiveTlsRefresh = settingsArg.aggressiveTlsRefresh !== undefined
1014
- ? settingsArg.aggressiveTlsRefresh
1015
- : isChainedProxy;
1016
-
1017
- // Configure TLS session cache if specified
1018
- if (settingsArg.tlsSessionCache) {
1019
- tlsSessionCache.updateConfig({
1020
- enabled: settingsArg.tlsSessionCache.enabled,
1021
- maxEntries: settingsArg.tlsSessionCache.maxEntries,
1022
- expiryTime: settingsArg.tlsSessionCache.expiryTime,
1023
- cleanupInterval: settingsArg.tlsSessionCache.cleanupInterval
1024
- });
1025
-
1026
- console.log(`Configured TLS session cache with custom settings. Current stats: ${JSON.stringify(tlsSessionCache.getStats())}`);
1027
- }
1028
-
1029
- // Determine appropriate timeouts based on proxy chain position
1030
- // Much more relaxed socket timeouts
1031
- let socketTimeout = 6 * 60 * 60 * 1000; // 6 hours default for standalone
1032
-
1033
- if (isChainedProxy) {
1034
- // Still adjust based on chain position, but with more relaxed values
1035
- const chainPosition = settingsArg.chainPosition || 'middle';
1036
-
1037
- // Adjust timeouts based on position in chain, but significantly relaxed
1038
- switch (chainPosition) {
1039
- case 'first':
1040
- // First proxy handling browser connections
1041
- socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
1042
- break;
1043
- case 'middle':
1044
- // Middle proxies
1045
- socketTimeout = 5 * 60 * 60 * 1000; // 5 hours
1046
- break;
1047
- case 'last':
1048
- // Last proxy connects to backend
1049
- socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
1050
- break;
1051
- }
1052
-
1053
- console.log(`Configured as ${chainPosition} proxy in chain. Using relaxed timeouts for better stability.`);
1054
- }
1055
-
1056
- // Set sensible defaults with significantly relaxed timeouts
356
+ // Set reasonable defaults for all settings
1057
357
  this.settings = {
1058
358
  ...settingsArg,
1059
- targetIP: targetIP,
1060
-
1061
- // Record the chained proxy status for use in other methods
1062
- isChainedProxy: isChainedProxy,
1063
- chainPosition: settingsArg.chainPosition || (isChainedProxy ? 'middle' : 'last'),
1064
- aggressiveTlsRefresh: aggressiveTlsRefresh,
1065
-
1066
- // Much more relaxed timeout settings
1067
- initialDataTimeout: 120000, // 2 minutes for initial handshake (doubled)
1068
- socketTimeout: socketTimeout, // 5-6 hours based on chain position
1069
- inactivityCheckInterval: 5 * 60 * 1000, // 5 minutes between checks (relaxed)
1070
- maxConnectionLifetime: 12 * 60 * 60 * 1000, // 12 hours lifetime
1071
- inactivityTimeout: 4 * 60 * 60 * 1000, // 4 hours inactivity timeout
1072
-
1073
- gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 60000, // 60 seconds
359
+ targetIP: settingsArg.targetIP || 'localhost',
360
+
361
+ // Timeout settings with reasonable defaults
362
+ initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake
363
+ socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout
364
+ inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
365
+ maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default
366
+ inactivityTimeout: ensureSafeTimeout(settingsArg.inactivityTimeout || 14400000), // 4 hours inactivity timeout
367
+
368
+ gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
1074
369
 
1075
370
  // Socket optimization settings
1076
371
  noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
1077
372
  keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
1078
- keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds (increased)
1079
- maxPendingDataSize: settingsArg.maxPendingDataSize || 20 * 1024 * 1024, // 20MB to handle large TLS handshakes
373
+ keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
374
+ maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
1080
375
 
1081
- // Feature flags - simplified with sensible defaults
1082
- disableInactivityCheck: false, // Still enable inactivity checks
1083
- enableKeepAliveProbes: true, // Still enable keep-alive probes
376
+ // Feature flags
377
+ disableInactivityCheck: settingsArg.disableInactivityCheck || false,
378
+ enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
379
+ ? settingsArg.enableKeepAliveProbes : true, // Enable by default
1084
380
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
1085
381
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
1086
- enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
382
+ enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
1087
383
 
1088
384
  // Rate limiting defaults
1089
- maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 200, // 200 connections per IP (doubled)
1090
- connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 500, // 500 per minute (increased)
1091
-
1092
- // Keep-alive settings with much more relaxed defaults
1093
- keepAliveTreatment: 'extended', // Use extended keep-alive treatment
1094
- keepAliveInactivityMultiplier: 3, // 3x normal inactivity timeout for longer extension
1095
- // Much longer keep-alive lifetimes
1096
- extendedKeepAliveLifetime: isChainedProxy
1097
- ? 24 * 60 * 60 * 1000 // 24 hours for chained proxies
1098
- : 48 * 60 * 60 * 1000, // 48 hours for standalone proxies
385
+ maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
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
1099
392
  };
1100
-
393
+
1101
394
  // Store NetworkProxy instances if provided
1102
395
  this.networkProxies = settingsArg.networkProxies || [];
1103
-
1104
- // Log proxy configuration details
1105
- console.log(`PortProxy initialized with ${isChainedProxy ? 'chained proxy' : 'standalone'} configuration.`);
1106
- if (isChainedProxy) {
1107
- console.log(`TLS certificate refresh: ${aggressiveTlsRefresh ? 'Aggressive' : 'Standard'}`);
1108
- console.log(`Connection lifetime: ${plugins.prettyMs(this.settings.maxConnectionLifetime)}`);
1109
- console.log(`Inactivity timeout: ${plugins.prettyMs(this.settings.inactivityTimeout)}`);
1110
- }
1111
396
  }
1112
397
 
1113
398
  /**
@@ -1128,93 +413,58 @@ export class PortProxy {
1128
413
  serverName?: string
1129
414
  ): void {
1130
415
  // Determine which NetworkProxy to use
1131
- const proxyIndex =
1132
- domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
1133
-
416
+ const proxyIndex = domainConfig.networkProxyIndex !== undefined
417
+ ? domainConfig.networkProxyIndex
418
+ : 0;
419
+
1134
420
  // Validate the NetworkProxy index
1135
421
  if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
1136
- console.log(
1137
- `[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
1138
- );
422
+ console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`);
1139
423
  // Fall back to direct connection
1140
- return this.setupDirectConnection(
1141
- connectionId,
1142
- socket,
1143
- record,
1144
- domainConfig,
1145
- serverName,
1146
- initialData
1147
- );
424
+ return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData);
1148
425
  }
1149
-
426
+
1150
427
  const networkProxy = this.networkProxies[proxyIndex];
1151
428
  const proxyPort = networkProxy.getListeningPort();
1152
429
  const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
1153
-
430
+
1154
431
  if (this.settings.enableDetailedLogging) {
1155
432
  console.log(
1156
433
  `[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
1157
434
  );
1158
435
  }
1159
-
1160
- // Create a connection to the NetworkProxy with optimized settings for reliability
436
+
437
+ // Create a connection to the NetworkProxy
1161
438
  const proxySocket = plugins.net.connect({
1162
439
  host: proxyHost,
1163
- port: proxyPort,
1164
- noDelay: true, // Disable Nagle's algorithm for NetworkProxy connections
1165
- keepAlive: this.settings.keepAlive, // Use the same keepAlive setting as regular connections
1166
- keepAliveInitialDelay: Math.max(this.settings.keepAliveInitialDelay - 5000, 5000) // Slightly faster
440
+ port: proxyPort
1167
441
  });
1168
-
442
+
1169
443
  // Store the outgoing socket in the record
1170
444
  record.outgoing = proxySocket;
1171
445
  record.outgoingStartTime = Date.now();
1172
446
  record.usingNetworkProxy = true;
1173
447
  record.networkProxyIndex = proxyIndex;
1174
448
 
1175
- // Mark keep-alive as enabled on outgoing if requested
1176
- if (this.settings.keepAlive) {
1177
- record.outgoingKeepAliveEnabled = true;
1178
-
1179
- // Apply enhanced TCP keep-alive options if enabled
1180
- if (this.settings.enableKeepAliveProbes) {
1181
- try {
1182
- if ('setKeepAliveProbes' in proxySocket) {
1183
- (proxySocket as any).setKeepAliveProbes(10);
1184
- }
1185
- if ('setKeepAliveInterval' in proxySocket) {
1186
- (proxySocket as any).setKeepAliveInterval(800);
1187
- }
1188
-
1189
- console.log(`[${connectionId}] Enhanced TCP keep-alive configured for NetworkProxy connection`);
1190
- } catch (err) {
1191
- // Ignore errors - these are optional enhancements
1192
- if (this.settings.enableDetailedLogging) {
1193
- console.log(`[${connectionId}] Enhanced keep-alive not supported for NetworkProxy: ${err}`);
1194
- }
1195
- }
1196
- }
1197
- }
1198
-
1199
449
  // Set up error handlers
1200
450
  proxySocket.on('error', (err) => {
1201
451
  console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
1202
452
  this.cleanupConnection(record, 'network_proxy_connect_error');
1203
453
  });
1204
-
454
+
1205
455
  // Handle connection to NetworkProxy
1206
456
  proxySocket.on('connect', () => {
1207
457
  if (this.settings.enableDetailedLogging) {
1208
458
  console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
1209
459
  }
1210
-
460
+
1211
461
  // First send the initial data that contains the TLS ClientHello
1212
462
  proxySocket.write(initialData);
1213
-
463
+
1214
464
  // Now set up bidirectional piping between client and NetworkProxy
1215
465
  socket.pipe(proxySocket);
1216
466
  proxySocket.pipe(socket);
1217
-
467
+
1218
468
  // Setup cleanup handlers
1219
469
  proxySocket.on('close', () => {
1220
470
  if (this.settings.enableDetailedLogging) {
@@ -1222,63 +472,18 @@ export class PortProxy {
1222
472
  }
1223
473
  this.cleanupConnection(record, 'network_proxy_closed');
1224
474
  });
1225
-
475
+
1226
476
  socket.on('close', () => {
1227
477
  if (this.settings.enableDetailedLogging) {
1228
- console.log(
1229
- `[${connectionId}] Client connection closed after forwarding to NetworkProxy`
1230
- );
478
+ console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`);
1231
479
  }
1232
480
  this.cleanupConnection(record, 'client_closed');
1233
481
  });
1234
482
 
1235
- // Special handler for TLS handshake detection with NetworkProxy
1236
- socket.on('data', (chunk: Buffer) => {
1237
- // Check for TLS handshake packets (ContentType.handshake)
1238
- if (chunk.length > 0 && chunk[0] === 22) {
1239
- console.log(`[${connectionId}] Detected potential TLS handshake with NetworkProxy, updating activity`);
1240
- this.updateActivity(record);
1241
- }
1242
- });
1243
-
1244
- // Update activity on data transfer from the proxy socket
483
+ // Update activity on data transfer
484
+ socket.on('data', () => this.updateActivity(record));
1245
485
  proxySocket.on('data', () => this.updateActivity(record));
1246
486
 
1247
- // Special handling for application-level keep-alives on NetworkProxy connections
1248
- if (this.settings.keepAlive && record.isTLS) {
1249
- // Set up a timer to periodically send application-level keep-alives
1250
- const keepAliveTimer = setInterval(() => {
1251
- if (proxySocket && !proxySocket.destroyed && record && !record.connectionClosed) {
1252
- try {
1253
- // Send 0-byte packet as application-level keep-alive
1254
- proxySocket.write(Buffer.alloc(0));
1255
-
1256
- if (this.settings.enableDetailedLogging) {
1257
- console.log(`[${connectionId}] Sent application-level keep-alive to NetworkProxy connection`);
1258
- }
1259
- } catch (err) {
1260
- // If we can't write, the connection is probably already dead
1261
- if (this.settings.enableDetailedLogging) {
1262
- console.log(`[${connectionId}] Error sending application-level keep-alive to NetworkProxy: ${err}`);
1263
- }
1264
-
1265
- // Stop the timer if we hit an error
1266
- clearInterval(keepAliveTimer);
1267
- }
1268
- } else {
1269
- // Clean up timer if connection is gone
1270
- clearInterval(keepAliveTimer);
1271
- }
1272
- }, 60000); // Send keep-alive every minute
1273
-
1274
- // Make sure interval doesn't prevent process exit
1275
- if (keepAliveTimer.unref) {
1276
- keepAliveTimer.unref();
1277
- }
1278
-
1279
- console.log(`[${connectionId}] Application-level keep-alive configured for NetworkProxy connection`);
1280
- }
1281
-
1282
487
  if (this.settings.enableDetailedLogging) {
1283
488
  console.log(
1284
489
  `[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
@@ -1286,7 +491,7 @@ export class PortProxy {
1286
491
  }
1287
492
  });
1288
493
  }
1289
-
494
+
1290
495
  /**
1291
496
  * Sets up a direct connection to the target (original behavior)
1292
497
  * This is used when NetworkProxy isn't configured or as a fallback
@@ -1300,37 +505,15 @@ export class PortProxy {
1300
505
  initialChunk?: Buffer,
1301
506
  overridePort?: number
1302
507
  ): void {
1303
- // Enhanced logging for initial connection troubleshooting
1304
- if (serverName) {
1305
- console.log(`[${connectionId}] Setting up direct connection for domain: ${serverName}`);
1306
- } else {
1307
- console.log(`[${connectionId}] Setting up direct connection without SNI`);
1308
- }
1309
-
1310
- // Log domain config details to help diagnose routing issues
1311
- if (domainConfig) {
1312
- console.log(`[${connectionId}] Using domain config: ${domainConfig.domains.join(', ')}`);
1313
- } else {
1314
- console.log(`[${connectionId}] No specific domain config found, using default settings`);
1315
- }
1316
-
1317
- // Ensure we maximize connection chances by setting appropriate timeouts
1318
- socket.setTimeout(30000); // 30 second initial connect timeout
1319
-
1320
508
  // Existing connection setup logic
1321
509
  const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
1322
510
  const connectionOptions: plugins.net.NetConnectOpts = {
1323
511
  host: targetHost,
1324
512
  port: overridePort !== undefined ? overridePort : this.settings.toPort,
1325
- // Add connection timeout to ensure we don't hang indefinitely
1326
- timeout: 15000 // 15 second connection timeout
1327
513
  };
1328
514
  if (this.settings.preserveSourceIP) {
1329
515
  connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
1330
516
  }
1331
-
1332
- console.log(`[${connectionId}] Connecting to backend: ${targetHost}:${connectionOptions.port}`);
1333
-
1334
517
 
1335
518
  // Pause the incoming socket to prevent buffer overflows
1336
519
  socket.pause();
@@ -1371,22 +554,11 @@ export class PortProxy {
1371
554
  // Add the temp handler to capture all incoming data during connection setup
1372
555
  socket.on('data', tempDataHandler);
1373
556
 
1374
- // Add initial chunk to pending data if present - this is critical for SNI forwarding
557
+ // Add initial chunk to pending data if present
1375
558
  if (initialChunk) {
1376
- // Make explicit copy of the buffer to ensure it doesn't get modified
1377
- const initialDataCopy = Buffer.from(initialChunk);
1378
- record.bytesReceived += initialDataCopy.length;
1379
- record.pendingData.push(initialDataCopy);
1380
- record.pendingDataSize = initialDataCopy.length;
1381
-
1382
- // Log TLS handshake for debug purposes
1383
- if (isTlsHandshake(initialChunk)) {
1384
- record.isTLS = true;
1385
- console.log(`[${connectionId}] Buffered TLS handshake data: ${initialDataCopy.length} bytes, SNI: ${serverName || 'none'}`);
1386
- }
1387
- } else if (record.isTLS) {
1388
- // This shouldn't happen, but log a warning if we have a TLS connection with no initial data
1389
- console.log(`[${connectionId}] WARNING: TLS connection without initial handshake data`);
559
+ record.bytesReceived += initialChunk.length;
560
+ record.pendingData.push(Buffer.from(initialChunk));
561
+ record.pendingDataSize = initialChunk.length;
1390
562
  }
1391
563
 
1392
564
  // Create the target socket but don't set up piping immediately
@@ -1396,77 +568,30 @@ export class PortProxy {
1396
568
 
1397
569
  // Apply socket optimizations
1398
570
  targetSocket.setNoDelay(this.settings.noDelay);
1399
-
571
+
1400
572
  // Apply keep-alive settings to the outgoing connection as well
1401
573
  if (this.settings.keepAlive) {
1402
- // Use a slightly shorter initial delay for outgoing to ensure it stays active
1403
- const outgoingInitialDelay = Math.max(this.settings.keepAliveInitialDelay - 5000, 5000);
1404
- targetSocket.setKeepAlive(true, outgoingInitialDelay);
1405
- record.outgoingKeepAliveEnabled = true;
574
+ targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
1406
575
 
1407
- console.log(`[${connectionId}] Keep-alive enabled on outgoing connection with initial delay: ${outgoingInitialDelay}ms`);
1408
-
1409
576
  // Apply enhanced TCP keep-alive options if enabled
1410
577
  if (this.settings.enableKeepAliveProbes) {
1411
578
  try {
1412
579
  if ('setKeepAliveProbes' in targetSocket) {
1413
- (targetSocket as any).setKeepAliveProbes(10); // Same probes as incoming
580
+ (targetSocket as any).setKeepAliveProbes(10);
1414
581
  }
1415
582
  if ('setKeepAliveInterval' in targetSocket) {
1416
- // Use a shorter interval on outgoing for more reliable detection
1417
- (targetSocket as any).setKeepAliveInterval(800); // Slightly faster than incoming
583
+ (targetSocket as any).setKeepAliveInterval(1000);
1418
584
  }
1419
-
1420
- console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on outgoing connection`);
1421
585
  } catch (err) {
1422
586
  // Ignore errors - these are optional enhancements
1423
587
  if (this.settings.enableDetailedLogging) {
1424
- console.log(
1425
- `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
1426
- );
588
+ console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
1427
589
  }
1428
590
  }
1429
591
  }
1430
-
1431
- // Special handling for TLS keep-alive - we want to be more aggressive
1432
- // with keeping the outgoing connection alive in TLS mode
1433
- if (record.isTLS) {
1434
- // Set a timer to periodically send empty data to keep connection alive
1435
- // This is in addition to TCP keep-alive, works at application layer
1436
- const keepAliveTimer = setInterval(() => {
1437
- if (targetSocket && !targetSocket.destroyed && record && !record.connectionClosed) {
1438
- try {
1439
- // Send 0-byte packet as application-level keep-alive
1440
- targetSocket.write(Buffer.alloc(0));
1441
-
1442
- if (this.settings.enableDetailedLogging) {
1443
- console.log(`[${connectionId}] Sent application-level keep-alive to outgoing TLS connection`);
1444
- }
1445
- } catch (err) {
1446
- // If we can't write, the connection is probably already dead
1447
- if (this.settings.enableDetailedLogging) {
1448
- console.log(`[${connectionId}] Error sending application-level keep-alive: ${err}`);
1449
- }
1450
-
1451
- // Stop the timer if we hit an error
1452
- clearInterval(keepAliveTimer);
1453
- }
1454
- } else {
1455
- // Clean up timer if connection is gone
1456
- clearInterval(keepAliveTimer);
1457
- }
1458
- }, 60000); // Send keep-alive every minute
1459
-
1460
- // Make sure interval doesn't prevent process exit
1461
- if (keepAliveTimer.unref) {
1462
- keepAliveTimer.unref();
1463
- }
1464
-
1465
- console.log(`[${connectionId}] Application-level keep-alive configured for TLS outgoing connection`);
1466
- }
1467
592
  }
1468
593
 
1469
- // Setup specific error handler for connection phase with enhanced retries
594
+ // Setup specific error handler for connection phase
1470
595
  targetSocket.once('error', (err) => {
1471
596
  // This handler runs only once during the initial connection phase
1472
597
  const code = (err as any).code;
@@ -1477,7 +602,6 @@ export class PortProxy {
1477
602
  // Resume the incoming socket to prevent it from hanging
1478
603
  socket.resume();
1479
604
 
1480
- // Add detailed logging for connection problems
1481
605
  if (code === 'ECONNREFUSED') {
1482
606
  console.log(
1483
607
  `[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
@@ -1493,28 +617,6 @@ export class PortProxy {
1493
617
  } else if (code === 'EHOSTUNREACH') {
1494
618
  console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
1495
619
  }
1496
-
1497
- // Log additional diagnostics
1498
- console.log(`[${connectionId}] Connection details - SNI: ${serverName || 'none'}, HasChunk: ${!!initialChunk}, ChunkSize: ${initialChunk ? initialChunk.length : 0}`);
1499
-
1500
- // For TLS connections, provide even more detailed diagnostics
1501
- if (record.isTLS) {
1502
- console.log(`[${connectionId}] TLS connection failure details - TLS detected: ${record.isTLS}, Server: ${targetHost}:${connectionOptions.port}, Domain config: ${domainConfig ? 'Present' : 'Missing'}`);
1503
- }
1504
-
1505
- // For connection refusal or timeouts, try a more aggressive error response
1506
- // This helps browsers quickly realize there's an issue rather than waiting
1507
- if (code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'EHOSTUNREACH') {
1508
- try {
1509
- // Send a RST packet rather than a graceful close
1510
- // This signals to browsers to try a new connection immediately
1511
- socket.destroy(new Error(`Backend connection failed: ${code}`));
1512
- console.log(`[${connectionId}] Forced connection termination to trigger immediate browser retry`);
1513
- return; // Skip normal cleanup
1514
- } catch (destroyErr) {
1515
- console.log(`[${connectionId}] Error during forced connection termination: ${destroyErr}`);
1516
- }
1517
- }
1518
620
 
1519
621
  // Clear any existing error handler after connection phase
1520
622
  targetSocket.removeAllListeners('error');
@@ -1540,21 +642,19 @@ export class PortProxy {
1540
642
  // For keep-alive connections, just log a warning instead of closing
1541
643
  if (record.hasKeepAlive) {
1542
644
  console.log(
1543
- `[${connectionId}] Timeout event on incoming keep-alive connection from ${
1544
- record.remoteIP
1545
- } after ${plugins.prettyMs(
645
+ `[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
1546
646
  this.settings.socketTimeout || 3600000
1547
647
  )}. Connection preserved.`
1548
648
  );
1549
649
  // Don't close the connection - just log
1550
650
  return;
1551
651
  }
1552
-
652
+
1553
653
  // For non-keep-alive connections, proceed with normal cleanup
1554
654
  console.log(
1555
- `[${connectionId}] Timeout on incoming side from ${
1556
- record.remoteIP
1557
- } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
655
+ `[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(
656
+ this.settings.socketTimeout || 3600000
657
+ )}`
1558
658
  );
1559
659
  if (record.incomingTerminationReason === null) {
1560
660
  record.incomingTerminationReason = 'timeout';
@@ -1567,21 +667,19 @@ export class PortProxy {
1567
667
  // For keep-alive connections, just log a warning instead of closing
1568
668
  if (record.hasKeepAlive) {
1569
669
  console.log(
1570
- `[${connectionId}] Timeout event on outgoing keep-alive connection from ${
1571
- record.remoteIP
1572
- } after ${plugins.prettyMs(
670
+ `[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
1573
671
  this.settings.socketTimeout || 3600000
1574
672
  )}. Connection preserved.`
1575
673
  );
1576
674
  // Don't close the connection - just log
1577
675
  return;
1578
676
  }
1579
-
677
+
1580
678
  // For non-keep-alive connections, proceed with normal cleanup
1581
679
  console.log(
1582
- `[${connectionId}] Timeout on outgoing side from ${
1583
- record.remoteIP
1584
- } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
680
+ `[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(
681
+ this.settings.socketTimeout || 3600000
682
+ )}`
1585
683
  );
1586
684
  if (record.outgoingTerminationReason === null) {
1587
685
  record.outgoingTerminationReason = 'timeout';
@@ -1595,11 +693,9 @@ export class PortProxy {
1595
693
  // Disable timeouts completely for immortal connections
1596
694
  socket.setTimeout(0);
1597
695
  targetSocket.setTimeout(0);
1598
-
696
+
1599
697
  if (this.settings.enableDetailedLogging) {
1600
- console.log(
1601
- `[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`
1602
- );
698
+ console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
1603
699
  }
1604
700
  } else {
1605
701
  // Set normal timeouts for other connections
@@ -1627,153 +723,14 @@ export class PortProxy {
1627
723
  // Flush all pending data to target
1628
724
  if (record.pendingData.length > 0) {
1629
725
  const combinedData = Buffer.concat(record.pendingData);
1630
-
1631
- // Add critical debugging for SNI forwarding issues
1632
- if (record.isTLS && this.settings.enableTlsDebugLogging) {
1633
- console.log(`[${connectionId}] Forwarding TLS handshake data: ${combinedData.length} bytes, SNI: ${serverName || 'none'}`);
1634
-
1635
- // Additional check to verify we're forwarding the ClientHello properly
1636
- if (combinedData[0] === 22) { // TLS handshake
1637
- console.log(`[${connectionId}] Initial data is a TLS handshake record`);
1638
- }
1639
- }
1640
-
1641
- // Write the combined data to the target
1642
726
  targetSocket.write(combinedData, (err) => {
1643
727
  if (err) {
1644
- console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
728
+ console.log(
729
+ `[${connectionId}] Error writing pending data to target: ${err.message}`
730
+ );
1645
731
  return this.initiateCleanupOnce(record, 'write_error');
1646
732
  }
1647
-
1648
- if (record.isTLS) {
1649
- // Log successful forwarding of initial TLS data
1650
- console.log(`[${connectionId}] Successfully forwarded initial TLS data to backend`);
1651
- }
1652
733
 
1653
- // Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
1654
- if (serverName && record.isTLS) {
1655
- // Create a flag to prevent double-processing of the same handshake packet
1656
- let processingRenegotiation = false;
1657
-
1658
- // This listener handles TLS renegotiation detection on the incoming socket
1659
- socket.on('data', (renegChunk) => {
1660
- // Only check for content type 22 (handshake) and not already processing
1661
- if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22 && !processingRenegotiation) {
1662
- processingRenegotiation = true;
1663
-
1664
- // Always update activity timestamp for any handshake packet
1665
- this.updateActivity(record);
1666
-
1667
- try {
1668
- // Enhanced logging for renegotiation
1669
- console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
1670
-
1671
- // Extract all TLS information including session resumption data
1672
- const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
1673
-
1674
- // Log details about the handshake packet
1675
- if (this.settings.enableTlsDebugLogging) {
1676
- console.log(`[${connectionId}] Handshake SNI extraction results:`, {
1677
- isResumption: sniInfo?.isResumption || false,
1678
- serverName: sniInfo?.serverName || 'none',
1679
- resumedDomain: sniInfo?.resumedDomain || 'none',
1680
- recordsExamined: sniInfo?.recordsExamined || 0,
1681
- multipleRecords: sniInfo?.multipleRecords || false,
1682
- partialExtract: sniInfo?.partialExtract || false
1683
- });
1684
- }
1685
-
1686
- let newSNI = sniInfo?.serverName;
1687
-
1688
- // Handle session resumption - if we recognize the session ID, we know what domain it belongs to
1689
- if (sniInfo?.isResumption && sniInfo.resumedDomain) {
1690
- console.log(`[${connectionId}] Rehandshake with session resumption for domain: ${sniInfo.resumedDomain}`);
1691
- newSNI = sniInfo.resumedDomain;
1692
- }
1693
-
1694
- // IMPORTANT: If we can't extract an SNI from renegotiation, but we detected a TLS handshake,
1695
- // we still need to make sure it's properly forwarded to maintain the TLS state
1696
- if (newSNI === undefined) {
1697
- console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
1698
-
1699
- // Set a temporary timeout to reset the processing flag
1700
- setTimeout(() => {
1701
- processingRenegotiation = false;
1702
- }, 500);
1703
-
1704
- return; // Let the piping handle the forwarding
1705
- }
1706
-
1707
- // Check if the SNI has changed
1708
- if (newSNI !== serverName) {
1709
- console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
1710
-
1711
- // Allow if the new SNI matches existing domain config or find a new matching config
1712
- let allowed = false;
1713
-
1714
- if (record.domainConfig) {
1715
- allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
1716
- }
1717
-
1718
- if (!allowed) {
1719
- const newDomainConfig = this.settings.domainConfigs.find((config) =>
1720
- config.domains.some((d) => plugins.minimatch(newSNI, d))
1721
- );
1722
-
1723
- if (newDomainConfig) {
1724
- const effectiveAllowedIPs = [
1725
- ...newDomainConfig.allowedIPs,
1726
- ...(this.settings.defaultAllowedIPs || []),
1727
- ];
1728
- const effectiveBlockedIPs = [
1729
- ...(newDomainConfig.blockedIPs || []),
1730
- ...(this.settings.defaultBlockedIPs || []),
1731
- ];
1732
-
1733
- allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
1734
-
1735
- if (allowed) {
1736
- record.domainConfig = newDomainConfig;
1737
- }
1738
- }
1739
- }
1740
-
1741
- if (allowed) {
1742
- console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
1743
- record.lockedDomain = newSNI;
1744
- } else {
1745
- console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
1746
- this.initiateCleanupOnce(record, 'sni_mismatch');
1747
- return;
1748
- }
1749
- } else {
1750
- console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
1751
- }
1752
- } catch (err) {
1753
- console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
1754
- } finally {
1755
- // Reset the processing flag after a small delay to prevent double-processing
1756
- // of packets that may be part of the same handshake
1757
- setTimeout(() => {
1758
- processingRenegotiation = false;
1759
- }, 500);
1760
- }
1761
- }
1762
- });
1763
-
1764
- // Set up a listener on the outgoing socket to detect issues with renegotiation
1765
- // This helps catch cases where the outgoing connection has closed but the incoming is still active
1766
- targetSocket.on('error', (err) => {
1767
- // If we get an error during what might be a renegotiation, log it specially
1768
- if (processingRenegotiation) {
1769
- console.log(`[${connectionId}] ERROR: Outgoing socket error during TLS renegotiation: ${err.message}`);
1770
- // Force immediate cleanup to prevent hanging connections
1771
- this.initiateCleanupOnce(record, 'renegotiation_error');
1772
- }
1773
- // The normal error handler will be called for other errors
1774
- });
1775
- }
1776
-
1777
734
  // Now set up piping for future data and resume the socket
1778
735
  socket.pipe(targetSocket);
1779
736
  targetSocket.pipe(socket);
@@ -1789,9 +746,7 @@ export class PortProxy {
1789
746
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1790
747
  : ''
1791
748
  }` +
1792
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1793
- record.hasKeepAlive ? 'Yes' : 'No'
1794
- }`
749
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
1795
750
  );
1796
751
  } else {
1797
752
  console.log(
@@ -1803,177 +758,11 @@ export class PortProxy {
1803
758
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1804
759
  : ''
1805
760
  }`
1806
- );
1807
- }
1808
- });
1809
- } else {
1810
- // Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
1811
- if (serverName && record.isTLS) {
1812
- // Create a flag to prevent double-processing of the same handshake packet
1813
- let processingRenegotiation = false;
1814
-
1815
- // This listener handles TLS renegotiation detection on the incoming socket
1816
- socket.on('data', (renegChunk) => {
1817
- // Only check for content type 22 (handshake) and not already processing
1818
- if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22 && !processingRenegotiation) {
1819
- processingRenegotiation = true;
1820
-
1821
- // Always update activity timestamp for any handshake packet
1822
- this.updateActivity(record);
1823
-
1824
- try {
1825
- // Enhanced logging for renegotiation
1826
- console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
1827
-
1828
- // Extract all TLS information including session resumption data
1829
- const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
1830
-
1831
- // Log details about the handshake packet
1832
- if (this.settings.enableTlsDebugLogging) {
1833
- console.log(`[${connectionId}] Handshake SNI extraction results:`, {
1834
- isResumption: sniInfo?.isResumption || false,
1835
- serverName: sniInfo?.serverName || 'none',
1836
- resumedDomain: sniInfo?.resumedDomain || 'none',
1837
- recordsExamined: sniInfo?.recordsExamined || 0,
1838
- multipleRecords: sniInfo?.multipleRecords || false,
1839
- partialExtract: sniInfo?.partialExtract || false
1840
- });
1841
- }
1842
-
1843
- let newSNI = sniInfo?.serverName;
1844
-
1845
- // Handle session resumption - if we recognize the session ID, we know what domain it belongs to
1846
- if (sniInfo?.isResumption && sniInfo.resumedDomain) {
1847
- console.log(`[${connectionId}] Rehandshake with session resumption for domain: ${sniInfo.resumedDomain}`);
1848
- newSNI = sniInfo.resumedDomain;
1849
- }
1850
-
1851
- // IMPORTANT: If we can't extract an SNI from renegotiation, but we detected a TLS handshake,
1852
- // we still need to make sure it's properly forwarded to maintain the TLS state
1853
- if (newSNI === undefined) {
1854
- console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
1855
-
1856
- // Set a temporary timeout to reset the processing flag
1857
- setTimeout(() => {
1858
- processingRenegotiation = false;
1859
- }, 500);
1860
-
1861
- return; // Let the piping handle the forwarding
1862
- }
1863
-
1864
- // Check if the SNI has changed
1865
- if (newSNI !== serverName) {
1866
- console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
1867
-
1868
- // Allow if the new SNI matches existing domain config or find a new matching config
1869
- let allowed = false;
1870
-
1871
- // First check if the new SNI is allowed under the existing domain config
1872
- // This is the preferred approach as it maintains the existing connection context
1873
- if (record.domainConfig) {
1874
- allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
1875
-
1876
- if (allowed) {
1877
- console.log(`[${connectionId}] Rehandshake SNI ${newSNI} allowed by existing domain config`);
1878
- }
1879
- }
1880
-
1881
- // If not allowed by existing config, try to find an alternative domain config
1882
- if (!allowed) {
1883
- // First try exact match
1884
- let newDomainConfig = this.settings.domainConfigs.find((config) =>
1885
- config.domains.some((d) => plugins.minimatch(newSNI, d))
1886
- );
1887
-
1888
- // If no exact match, try flexible matching with domain parts (for wildcard domains)
1889
- if (!newDomainConfig) {
1890
- console.log(`[${connectionId}] No exact domain config match for rehandshake SNI: ${newSNI}, trying flexible matching`);
1891
-
1892
- const domainParts = newSNI.split('.');
1893
-
1894
- // Try matching with parent domains or wildcard patterns
1895
- if (domainParts.length > 2) {
1896
- const parentDomain = domainParts.slice(1).join('.');
1897
- const wildcardDomain = '*.' + parentDomain;
1898
-
1899
- console.log(`[${connectionId}] Trying alternative patterns: ${parentDomain} or ${wildcardDomain}`);
1900
-
1901
- newDomainConfig = this.settings.domainConfigs.find((config) =>
1902
- config.domains.some((d) =>
1903
- d === parentDomain ||
1904
- d === wildcardDomain ||
1905
- plugins.minimatch(parentDomain, d)
1906
- )
1907
- );
1908
- }
1909
- }
1910
-
1911
- if (newDomainConfig) {
1912
- const effectiveAllowedIPs = [
1913
- ...newDomainConfig.allowedIPs,
1914
- ...(this.settings.defaultAllowedIPs || []),
1915
- ];
1916
- const effectiveBlockedIPs = [
1917
- ...(newDomainConfig.blockedIPs || []),
1918
- ...(this.settings.defaultBlockedIPs || []),
1919
- ];
1920
-
1921
- allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
1922
-
1923
- if (allowed) {
1924
- record.domainConfig = newDomainConfig;
1925
- }
1926
- }
1927
- }
1928
-
1929
- if (allowed) {
1930
- console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
1931
- record.lockedDomain = newSNI;
1932
- } else {
1933
- console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
1934
- this.initiateCleanupOnce(record, 'sni_mismatch');
1935
- return;
1936
- }
1937
- } else {
1938
- console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
1939
- }
1940
- } catch (err) {
1941
- console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
1942
- } finally {
1943
- // Reset the processing flag after a small delay to prevent double-processing
1944
- // of packets that may be part of the same handshake
1945
- setTimeout(() => {
1946
- processingRenegotiation = false;
1947
- }, 500);
1948
- }
1949
- }
1950
- });
1951
-
1952
- // Set up a listener on the outgoing socket to detect issues with renegotiation
1953
- // This helps catch cases where the outgoing connection has closed but the incoming is still active
1954
- targetSocket.on('error', (err) => {
1955
- // If we get an error during what might be a renegotiation, log it specially
1956
- if (processingRenegotiation) {
1957
- console.log(`[${connectionId}] ERROR: Outgoing socket error during TLS renegotiation: ${err.message}`);
1958
- // Force immediate cleanup to prevent hanging connections
1959
- this.initiateCleanupOnce(record, 'renegotiation_error');
1960
- }
1961
- // The normal error handler will be called for other errors
1962
- });
1963
-
1964
- // Also monitor targetSocket for connection issues during client handshakes
1965
- targetSocket.on('close', () => {
1966
- // If the outgoing socket closes during renegotiation, it's a critical issue
1967
- if (processingRenegotiation) {
1968
- console.log(`[${connectionId}] CRITICAL: Outgoing socket closed during TLS renegotiation!`);
1969
- console.log(`[${connectionId}] This likely explains cert mismatch errors in the browser.`);
1970
- // Force immediate cleanup on the client side
1971
- this.initiateCleanupOnce(record, 'target_closed_during_renegotiation');
1972
- }
1973
- });
1974
- }
1975
-
1976
- // Now set up piping
761
+ );
762
+ }
763
+ });
764
+ } else {
765
+ // No pending data, so just set up piping
1977
766
  socket.pipe(targetSocket);
1978
767
  targetSocket.pipe(socket);
1979
768
  socket.resume(); // Resume the socket after piping is established
@@ -1988,9 +777,7 @@ export class PortProxy {
1988
777
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1989
778
  : ''
1990
779
  }` +
1991
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1992
- record.hasKeepAlive ? 'Yes' : 'No'
1993
- }`
780
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
1994
781
  );
1995
782
  } else {
1996
783
  console.log(
@@ -2010,98 +797,82 @@ export class PortProxy {
2010
797
  record.pendingData = [];
2011
798
  record.pendingDataSize = 0;
2012
799
 
2013
- // Renegotiation detection is now handled before piping is established
2014
- // This ensures the data listener receives all packets properly
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
+ }
2015
825
 
2016
826
  // Set connection timeout with simpler logic
2017
827
  if (record.cleanupTimer) {
2018
828
  clearTimeout(record.cleanupTimer);
2019
829
  }
2020
-
830
+
2021
831
  // For immortal keep-alive connections, skip setting a timeout completely
2022
832
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
2023
833
  if (this.settings.enableDetailedLogging) {
2024
- console.log(
2025
- `[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
2026
- );
834
+ console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
2027
835
  }
2028
836
  // No cleanup timer for immortal connections
2029
- }
2030
- // For TLS keep-alive connections, use a more generous timeout now that
2031
- // we've fixed the renegotiation handling issue that was causing certificate problems
2032
- else if (record.hasKeepAlive && record.isTLS) {
2033
- // Use a longer timeout for TLS connections now that renegotiation handling is fixed
2034
- // This reduces unnecessary reconnections while still ensuring certificate freshness
2035
- const tlsKeepAliveTimeout = 4 * 60 * 60 * 1000; // 4 hours for TLS keep-alive - increased from 30 minutes
2036
- const safeTimeout = ensureSafeTimeout(tlsKeepAliveTimeout);
2037
-
2038
- record.cleanupTimer = setTimeout(() => {
2039
- console.log(
2040
- `[${connectionId}] TLS keep-alive connection from ${
2041
- record.remoteIP
2042
- } exceeded max lifetime (${plugins.prettyMs(
2043
- tlsKeepAliveTimeout
2044
- )}), forcing cleanup to refresh certificate context.`
2045
- );
2046
- this.initiateCleanupOnce(record, 'tls_certificate_refresh');
2047
- }, safeTimeout);
2048
-
2049
- // Make sure timeout doesn't keep the process alive
2050
- if (record.cleanupTimer.unref) {
2051
- record.cleanupTimer.unref();
2052
- }
2053
-
2054
- if (this.settings.enableDetailedLogging) {
2055
- console.log(
2056
- `[${connectionId}] TLS keep-alive connection with aggressive certificate refresh protection, lifetime: ${plugins.prettyMs(
2057
- tlsKeepAliveTimeout
2058
- )}`
2059
- );
2060
- }
2061
- }
837
+ }
2062
838
  // For extended keep-alive connections, use extended timeout
2063
839
  else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
2064
840
  const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
2065
841
  const safeTimeout = ensureSafeTimeout(extendedTimeout);
2066
-
842
+
2067
843
  record.cleanupTimer = setTimeout(() => {
2068
844
  console.log(
2069
- `[${connectionId}] Keep-alive connection from ${
2070
- record.remoteIP
2071
- } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
845
+ `[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs(
846
+ extendedTimeout
847
+ )}), forcing cleanup.`
2072
848
  );
2073
849
  this.initiateCleanupOnce(record, 'extended_lifetime');
2074
850
  }, safeTimeout);
2075
-
851
+
2076
852
  // Make sure timeout doesn't keep the process alive
2077
853
  if (record.cleanupTimer.unref) {
2078
854
  record.cleanupTimer.unref();
2079
855
  }
2080
-
856
+
2081
857
  if (this.settings.enableDetailedLogging) {
2082
- console.log(
2083
- `[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
2084
- extendedTimeout
2085
- )}`
2086
- );
858
+ console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
2087
859
  }
2088
860
  }
2089
861
  // For standard connections, use normal timeout
2090
862
  else {
2091
863
  // Use domain-specific timeout if available, otherwise use default
2092
- const connectionTimeout =
2093
- record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
864
+ const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
2094
865
  const safeTimeout = ensureSafeTimeout(connectionTimeout);
2095
-
866
+
2096
867
  record.cleanupTimer = setTimeout(() => {
2097
868
  console.log(
2098
- `[${connectionId}] Connection from ${
2099
- record.remoteIP
2100
- } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
869
+ `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs(
870
+ connectionTimeout
871
+ )}), forcing cleanup.`
2101
872
  );
2102
873
  this.initiateCleanupOnce(record, 'connection_timeout');
2103
874
  }, safeTimeout);
2104
-
875
+
2105
876
  // Make sure timeout doesn't keep the process alive
2106
877
  if (record.cleanupTimer.unref) {
2107
878
  record.cleanupTimer.unref();
@@ -2179,220 +950,6 @@ export class PortProxy {
2179
950
  this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
2180
951
  }
2181
952
 
2182
- /**
2183
- * Update connection activity timestamp with enhanced sleep detection
2184
- * Improved for chained proxy scenarios and more aggressive handling of stale connections
2185
- */
2186
- private updateActivity(record: IConnectionRecord): void {
2187
- // Get the current time
2188
- const now = Date.now();
2189
-
2190
- // Check if there was a large time gap that suggests system sleep
2191
- if (record.lastActivity > 0) {
2192
- const timeDiff = now - record.lastActivity;
2193
-
2194
- // Enhanced sleep detection with graduated thresholds - much more relaxed
2195
- // Using chain detection from settings instead of recalculating
2196
- const isChainedProxy = this.settings.isChainedProxy || false;
2197
- const minuteInMs = 60 * 1000;
2198
- const hourInMs = 60 * minuteInMs;
2199
-
2200
- // Significantly relaxed thresholds for better stability
2201
- const shortInactivityThreshold = 30 * minuteInMs; // 30 minutes
2202
- const mediumInactivityThreshold = 2 * hourInMs; // 2 hours
2203
- const longInactivityThreshold = 8 * hourInMs; // 8 hours
2204
-
2205
- // Short inactivity (10-15 mins) - Might be temporary network issue or short sleep
2206
- if (timeDiff > shortInactivityThreshold) {
2207
- if (record.isTLS && !record.possibleSystemSleep) {
2208
- // Record first detection of possible sleep/inactivity
2209
- record.possibleSystemSleep = true;
2210
- record.lastSleepDetection = now;
2211
-
2212
- if (this.settings.enableDetailedLogging) {
2213
- console.log(
2214
- `[${record.id}] Detected possible short inactivity for ${plugins.prettyMs(timeDiff)}. ` +
2215
- `Monitoring for TLS connection health.`
2216
- );
2217
- }
2218
-
2219
- // For TLS connections, send a minimal probe to check connection health
2220
- if (!record.usingNetworkProxy && record.outgoing && !record.outgoing.destroyed) {
2221
- try {
2222
- record.outgoing.write(Buffer.alloc(0));
2223
- } catch (err) {
2224
- console.log(`[${record.id}] Error sending TLS probe: ${err}`);
2225
- }
2226
- }
2227
- }
2228
- }
2229
-
2230
- // Medium inactivity (20-30 mins) - Likely a sleep event or network change
2231
- if (timeDiff > mediumInactivityThreshold && record.hasKeepAlive) {
2232
- console.log(
2233
- `[${record.id}] Detected medium inactivity period for ${plugins.prettyMs(timeDiff)}. ` +
2234
- `Taking proactive steps for connection health.`
2235
- );
2236
-
2237
- // For TLS connections, we need more aggressive handling
2238
- if (record.isTLS && record.tlsHandshakeComplete) {
2239
- // If in a chained proxy, we should be even more aggressive about refreshing
2240
- if (isChainedProxy) {
2241
- console.log(
2242
- `[${record.id}] TLS connection in chained proxy inactive for ${plugins.prettyMs(timeDiff)}. ` +
2243
- `Closing to prevent certificate inconsistencies across chain.`
2244
- );
2245
- return this.initiateCleanupOnce(record, 'chained_proxy_inactivity');
2246
- }
2247
-
2248
- // For TLS in single proxy, try refresh first
2249
- console.log(
2250
- `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
2251
- `Attempting active refresh of TLS state.`
2252
- );
2253
-
2254
- // Attempt deep TLS state refresh with buffer flush
2255
- this.performDeepTlsRefresh(record);
2256
-
2257
- // Schedule verification check with tighter timing for chained setups
2258
- const verificationTimeout = isChainedProxy ? 5 * minuteInMs : 10 * minuteInMs;
2259
- const refreshCheckId = record.id;
2260
- const refreshCheck = setTimeout(() => {
2261
- const currentRecord = this.connectionRecords.get(refreshCheckId);
2262
- if (currentRecord) {
2263
- const verificationTimeDiff = Date.now() - currentRecord.lastActivity;
2264
- if (verificationTimeDiff > verificationTimeout / 2) {
2265
- console.log(
2266
- `[${refreshCheckId}] No activity detected after TLS refresh (${plugins.prettyMs(verificationTimeDiff)}). ` +
2267
- `Closing connection to ensure proper browser reconnection.`
2268
- );
2269
- this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
2270
- }
2271
- }
2272
- }, verificationTimeout);
2273
-
2274
- // Make sure timeout doesn't keep the process alive
2275
- if (refreshCheck.unref) {
2276
- refreshCheck.unref();
2277
- }
2278
- }
2279
-
2280
- // Update sleep detection markers
2281
- record.possibleSystemSleep = true;
2282
- record.lastSleepDetection = now;
2283
- }
2284
-
2285
- // Long inactivity (60-120 mins) - Definite sleep/suspend or major network change
2286
- if (timeDiff > longInactivityThreshold) {
2287
- console.log(
2288
- `[${record.id}] Detected long inactivity period of ${plugins.prettyMs(timeDiff)}. ` +
2289
- `Closing connection to ensure fresh certificate context.`
2290
- );
2291
-
2292
- // For long periods, we always want to force close and let browser reconnect
2293
- // This ensures fresh certificates and proper TLS context across the chain
2294
- return this.initiateCleanupOnce(record, 'extended_inactivity_refresh');
2295
- }
2296
- }
2297
-
2298
- // Update the activity timestamp
2299
- record.lastActivity = now;
2300
-
2301
- // Clear any inactivity warning
2302
- if (record.inactivityWarningIssued) {
2303
- record.inactivityWarningIssued = false;
2304
- }
2305
- }
2306
-
2307
- /**
2308
- * Perform deep TLS state refresh after sleep detection
2309
- * More aggressive than the standard refresh, specifically designed for
2310
- * recovering connections after system sleep in chained proxy setups
2311
- */
2312
- private performDeepTlsRefresh(record: IConnectionRecord): void {
2313
- // Skip if we're using a NetworkProxy as it handles its own TLS state
2314
- if (record.usingNetworkProxy) {
2315
- return;
2316
- }
2317
-
2318
- try {
2319
- // For outgoing connections that might need to be refreshed
2320
- if (record.outgoing && !record.outgoing.destroyed) {
2321
- // Check how long this connection has been established
2322
- const connectionAge = Date.now() - record.incomingStartTime;
2323
- const hourInMs = 60 * 60 * 1000;
2324
-
2325
- // For very long-lived connections, just close them
2326
- if (connectionAge > 4 * hourInMs) { // Reduced from 8 hours to 4 hours for chained proxies
2327
- console.log(
2328
- `[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
2329
- `Closing to ensure proper certificate handling across proxy chain.`
2330
- );
2331
- return this.initiateCleanupOnce(record, 'certificate_age_refresh');
2332
- }
2333
-
2334
- // Perform a series of actions to try to refresh the TLS state
2335
-
2336
- // 1. Send a zero-length buffer to trigger any pending errors
2337
- record.outgoing.write(Buffer.alloc(0));
2338
-
2339
- // 2. Check socket state
2340
- if (record.outgoing.writableEnded || !record.outgoing.writable) {
2341
- console.log(`[${record.id}] Socket no longer writable during refresh`);
2342
- return this.initiateCleanupOnce(record, 'socket_state_error');
2343
- }
2344
-
2345
- // 3. For TLS connections, try to force background renegotiation
2346
- // by manipulating socket timeouts
2347
- const originalTimeout = record.outgoing.timeout;
2348
- record.outgoing.setTimeout(100); // Set very short timeout
2349
-
2350
- // 4. Create a small delay to allow timeout to process
2351
- setTimeout(() => {
2352
- try {
2353
- if (record.outgoing && !record.outgoing.destroyed) {
2354
- // Reset timeout to original value
2355
- record.outgoing.setTimeout(originalTimeout || 0);
2356
-
2357
- // Send another probe with random data (16 bytes) that will be ignored by TLS layer
2358
- // but might trigger internal state updates in the TLS implementation
2359
- const probeBuffer = Buffer.alloc(16);
2360
- // Fill with random data
2361
- for (let i = 0; i < 16; i++) {
2362
- probeBuffer[i] = Math.floor(Math.random() * 256);
2363
- }
2364
- record.outgoing.write(Buffer.alloc(0));
2365
-
2366
- if (this.settings.enableDetailedLogging) {
2367
- console.log(`[${record.id}] Completed deep TLS refresh sequence`);
2368
- }
2369
- }
2370
- } catch (innerErr) {
2371
- console.log(`[${record.id}] Error during deep TLS refresh: ${innerErr}`);
2372
- this.initiateCleanupOnce(record, 'deep_refresh_error');
2373
- }
2374
- }, 150);
2375
-
2376
- if (this.settings.enableDetailedLogging) {
2377
- console.log(`[${record.id}] Initiated deep TLS refresh sequence`);
2378
- }
2379
- }
2380
- } catch (err) {
2381
- console.log(`[${record.id}] Error starting TLS state refresh: ${err}`);
2382
-
2383
- // If we hit an error, it's likely the connection is already broken
2384
- // Force cleanup to ensure browser reconnects cleanly
2385
- return this.initiateCleanupOnce(record, 'tls_refresh_error');
2386
- }
2387
- }
2388
-
2389
- /**
2390
- * Legacy refresh method for backward compatibility
2391
- */
2392
- private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
2393
- return this.performDeepTlsRefresh(record);
2394
- }
2395
-
2396
953
  /**
2397
954
  * Cleans up a connection record.
2398
955
  * Destroys both incoming and outgoing sockets, clears timers, and removes the record.
@@ -2490,9 +1047,7 @@ export class PortProxy {
2490
1047
  ` Duration: ${plugins.prettyMs(
2491
1048
  duration
2492
1049
  )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
2493
- `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
2494
- record.hasKeepAlive ? 'Yes' : 'No'
2495
- }` +
1050
+ `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
2496
1051
  `${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
2497
1052
  );
2498
1053
  } else {
@@ -2503,6 +1058,18 @@ export class PortProxy {
2503
1058
  }
2504
1059
  }
2505
1060
 
1061
+ /**
1062
+ * Update connection activity timestamp
1063
+ */
1064
+ private updateActivity(record: IConnectionRecord): void {
1065
+ record.lastActivity = Date.now();
1066
+
1067
+ // Clear any inactivity warning
1068
+ if (record.inactivityWarningIssued) {
1069
+ record.inactivityWarningIssued = false;
1070
+ }
1071
+ }
1072
+
2506
1073
  /**
2507
1074
  * Get target IP with round-robin support
2508
1075
  */
@@ -2515,7 +1082,7 @@ export class PortProxy {
2515
1082
  }
2516
1083
  return this.settings.targetIP!;
2517
1084
  }
2518
-
1085
+
2519
1086
  /**
2520
1087
  * Initiates cleanup once for a connection
2521
1088
  */
@@ -2523,15 +1090,12 @@ export class PortProxy {
2523
1090
  if (this.settings.enableDetailedLogging) {
2524
1091
  console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
2525
1092
  }
2526
-
2527
- if (
2528
- record.incomingTerminationReason === null ||
2529
- record.incomingTerminationReason === undefined
2530
- ) {
1093
+
1094
+ if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) {
2531
1095
  record.incomingTerminationReason = reason;
2532
1096
  this.incrementTerminationStat('incoming', reason);
2533
1097
  }
2534
-
1098
+
2535
1099
  this.cleanupConnection(record, reason);
2536
1100
  }
2537
1101
 
@@ -2655,7 +1219,7 @@ export class PortProxy {
2655
1219
 
2656
1220
  // Apply socket optimizations
2657
1221
  socket.setNoDelay(this.settings.noDelay);
2658
-
1222
+
2659
1223
  // Create a unique connection ID and record
2660
1224
  const connectionId = generateConnectionId();
2661
1225
  const connectionRecord: IConnectionRecord = {
@@ -2679,27 +1243,16 @@ export class PortProxy {
2679
1243
  hasKeepAlive: false, // Will set to true if keep-alive is applied
2680
1244
  incomingTerminationReason: null,
2681
1245
  outgoingTerminationReason: null,
2682
-
2683
- // Initialize NetworkProxy tracking fields
2684
- usingNetworkProxy: false,
2685
-
2686
- // Initialize sleep detection fields
2687
- possibleSystemSleep: false,
2688
1246
 
2689
- // Track keep-alive state for both sides of the connection
2690
- incomingKeepAliveEnabled: false,
2691
- outgoingKeepAliveEnabled: false,
1247
+ // Initialize NetworkProxy tracking fields
1248
+ usingNetworkProxy: false
2692
1249
  };
2693
-
1250
+
2694
1251
  // Apply keep-alive settings if enabled
2695
1252
  if (this.settings.keepAlive) {
2696
- // Configure incoming socket keep-alive
2697
1253
  socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
2698
1254
  connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
2699
- connectionRecord.incomingKeepAliveEnabled = true;
2700
1255
 
2701
- console.log(`[${connectionId}] Keep-alive enabled on incoming connection with initial delay: ${this.settings.keepAliveInitialDelay}ms`);
2702
-
2703
1256
  // Apply enhanced TCP keep-alive options if enabled
2704
1257
  if (this.settings.enableKeepAliveProbes) {
2705
1258
  try {
@@ -2710,14 +1263,10 @@ export class PortProxy {
2710
1263
  if ('setKeepAliveInterval' in socket) {
2711
1264
  (socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
2712
1265
  }
2713
-
2714
- console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on incoming connection`);
2715
1266
  } catch (err) {
2716
1267
  // Ignore errors - these are optional enhancements
2717
1268
  if (this.settings.enableDetailedLogging) {
2718
- console.log(
2719
- `[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
2720
- );
1269
+ console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
2721
1270
  }
2722
1271
  }
2723
1272
  }
@@ -2730,8 +1279,8 @@ export class PortProxy {
2730
1279
  if (this.settings.enableDetailedLogging) {
2731
1280
  console.log(
2732
1281
  `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
2733
- `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
2734
- `Active connections: ${this.connectionRecords.size}`
1282
+ `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
1283
+ `Active connections: ${this.connectionRecords.size}`
2735
1284
  );
2736
1285
  } else {
2737
1286
  console.log(
@@ -2835,95 +1384,16 @@ export class PortProxy {
2835
1384
  }
2836
1385
 
2837
1386
  // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
2838
- let domainConfig = forcedDomain
1387
+ const domainConfig = forcedDomain
2839
1388
  ? forcedDomain
2840
1389
  : serverName
2841
1390
  ? this.settings.domainConfigs.find((config) =>
2842
1391
  config.domains.some((d) => plugins.minimatch(serverName, d))
2843
1392
  )
2844
1393
  : undefined;
2845
-
2846
- // Enhanced logging to diagnose domain config selection issues
2847
- if (serverName && !domainConfig) {
2848
- console.log(`[${connectionId}] WARNING: No domain config found for SNI: ${serverName}`);
2849
- console.log(`[${connectionId}] Available domains:`,
2850
- this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
2851
- } else if (serverName && domainConfig) {
2852
- console.log(`[${connectionId}] Found domain config for SNI: ${serverName} -> ${domainConfig.domains.join(',')}`);
2853
- }
2854
1394
 
2855
- // For session resumption, ensure we use the domain config matching the resumed domain
2856
- // The resumed domain will be in serverName if this is a session resumption
2857
- if (serverName && connectionRecord.lockedDomain === serverName && serverName !== '') {
2858
- // Override domain config lookup for session resumption - crucial for certificate selection
2859
-
2860
- // First try an exact match
2861
- let resumedDomainConfig = this.settings.domainConfigs.find((config) =>
2862
- config.domains.some((d) => plugins.minimatch(serverName, d))
2863
- );
2864
-
2865
- // If no exact match found, try a more flexible approach using domain parts
2866
- if (!resumedDomainConfig) {
2867
- console.log(`[${connectionId}] No exact domain config match for resumed domain: ${serverName}, trying flexible matching`);
2868
-
2869
- // Extract domain parts (e.g., for "sub.example.com" try matching with "*.example.com")
2870
- const domainParts = serverName.split('.');
2871
-
2872
- // Try matching with parent domains or wildcard patterns
2873
- if (domainParts.length > 2) {
2874
- const parentDomain = domainParts.slice(1).join('.');
2875
- const wildcardDomain = '*.' + parentDomain;
2876
-
2877
- console.log(`[${connectionId}] Trying alternative patterns: ${parentDomain} or ${wildcardDomain}`);
2878
-
2879
- resumedDomainConfig = this.settings.domainConfigs.find((config) =>
2880
- config.domains.some((d) =>
2881
- d === parentDomain ||
2882
- d === wildcardDomain ||
2883
- plugins.minimatch(parentDomain, d)
2884
- )
2885
- );
2886
- }
2887
- }
2888
-
2889
- if (resumedDomainConfig) {
2890
- domainConfig = resumedDomainConfig;
2891
- console.log(`[${connectionId}] Found domain config for resumed session: ${serverName} -> ${resumedDomainConfig.domains.join(',')}`);
2892
- } else {
2893
- // As a fallback, use the first domain config with the same target IP if possible
2894
- if (domainConfig && domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
2895
- const targetIP = domainConfig.targetIPs[0];
2896
-
2897
- const similarConfig = this.settings.domainConfigs.find((config) =>
2898
- config.targetIPs && config.targetIPs.includes(targetIP)
2899
- );
2900
-
2901
- if (similarConfig && similarConfig !== domainConfig) {
2902
- console.log(`[${connectionId}] Using similar domain config with matching target IP for resumed domain: ${serverName}`);
2903
- domainConfig = similarConfig;
2904
- } else {
2905
- console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
2906
- // Log available domains to help diagnose the issue
2907
- console.log(`[${connectionId}] Available domains:`,
2908
- this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
2909
- }
2910
- } else {
2911
- console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
2912
- // Log available domains to help diagnose the issue
2913
- console.log(`[${connectionId}] Available domains:`,
2914
- this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
2915
- }
2916
- }
2917
- }
2918
-
2919
1395
  // Save domain config in connection record
2920
1396
  connectionRecord.domainConfig = domainConfig;
2921
-
2922
- // Always set the lockedDomain, even for non-SNI connections
2923
- if (serverName) {
2924
- connectionRecord.lockedDomain = serverName;
2925
- console.log(`[${connectionId}] Locked connection to domain: ${serverName}`);
2926
- }
2927
1397
 
2928
1398
  // IP validation is skipped if allowedIPs is empty
2929
1399
  if (domainConfig) {
@@ -2948,12 +1418,12 @@ export class PortProxy {
2948
1418
  )}`
2949
1419
  );
2950
1420
  }
2951
-
1421
+
2952
1422
  // Check if we should forward this to a NetworkProxy
2953
1423
  if (
2954
- isTlsHandshakeDetected &&
2955
- domainConfig.useNetworkProxy === true &&
2956
- initialChunk &&
1424
+ isTlsHandshakeDetected &&
1425
+ domainConfig.useNetworkProxy === true &&
1426
+ initialChunk &&
2957
1427
  this.networkProxies.length > 0
2958
1428
  ) {
2959
1429
  return this.forwardToNetworkProxy(
@@ -3080,49 +1550,19 @@ export class PortProxy {
3080
1550
 
3081
1551
  initialDataReceived = true;
3082
1552
 
3083
- // Try to extract SNI - with enhanced logging for troubleshooting
1553
+ // Try to extract SNI
3084
1554
  let serverName = '';
3085
-
3086
- // Record the chunk size for diagnostic purposes
3087
- console.log(`[${connectionId}] Received initial data: ${chunk.length} bytes`);
3088
1555
 
3089
1556
  if (isTlsHandshake(chunk)) {
3090
1557
  connectionRecord.isTLS = true;
3091
1558
 
3092
- console.log(`[${connectionId}] Detected TLS handshake`);
3093
-
3094
1559
  if (this.settings.enableTlsDebugLogging) {
3095
1560
  console.log(
3096
1561
  `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
3097
1562
  );
3098
1563
  }
3099
1564
 
3100
- // Extract all TLS information including session resumption
3101
- const sniInfo = extractSNIInfo(chunk, this.settings.enableTlsDebugLogging);
3102
-
3103
- if (sniInfo?.isResumption && sniInfo.resumedDomain) {
3104
- // This is a session resumption with a known domain
3105
- serverName = sniInfo.resumedDomain;
3106
- console.log(`[${connectionId}] TLS Session resumption detected for domain: ${serverName}`);
3107
-
3108
- // When resuming a session, explicitly set the domain in the record to ensure proper routing
3109
- // This is CRITICAL for ensuring we select the correct backend/certificate
3110
- connectionRecord.lockedDomain = serverName;
3111
-
3112
- // Force detailed logging for resumed sessions to help with troubleshooting
3113
- console.log(`[${connectionId}] Resuming TLS session for domain ${serverName} - will use original certificate`);
3114
- } else {
3115
- // Normal SNI extraction
3116
- serverName = sniInfo?.serverName || '';
3117
-
3118
- if (serverName) {
3119
- console.log(`[${connectionId}] Extracted SNI domain: ${serverName}`);
3120
- } else {
3121
- console.log(`[${connectionId}] No SNI found in TLS handshake`);
3122
- }
3123
- }
3124
- } else {
3125
- console.log(`[${connectionId}] Non-TLS connection detected`);
1565
+ serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
3126
1566
  }
3127
1567
 
3128
1568
  // Lock the connection to the negotiated SNI.
@@ -3221,11 +1661,11 @@ export class PortProxy {
3221
1661
  } else {
3222
1662
  nonTlsConnections++;
3223
1663
  }
3224
-
1664
+
3225
1665
  if (record.hasKeepAlive) {
3226
1666
  keepAliveConnections++;
3227
1667
  }
3228
-
1668
+
3229
1669
  if (record.usingNetworkProxy) {
3230
1670
  networkProxyConnections++;
3231
1671
  }
@@ -3266,80 +1706,35 @@ export class PortProxy {
3266
1706
  }
3267
1707
 
3268
1708
  // Skip inactivity check if disabled or for immortal keep-alive connections
3269
- if (
3270
- !this.settings.disableInactivityCheck &&
3271
- !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
3272
- ) {
1709
+ if (!this.settings.disableInactivityCheck &&
1710
+ !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
1711
+
3273
1712
  const inactivityTime = now - record.lastActivity;
3274
-
3275
- // Special handling for TLS keep-alive connections
3276
- if (
3277
- record.hasKeepAlive &&
3278
- record.isTLS &&
3279
- inactivityTime > this.settings.inactivityTimeout! / 2
3280
- ) {
3281
- // For TLS keep-alive connections that are getting stale, try to refresh before closing
3282
- if (!record.inactivityWarningIssued) {
3283
- console.log(
3284
- `[${id}] TLS keep-alive connection from ${
3285
- record.remoteIP
3286
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
3287
- `Attempting to preserve connection.`
3288
- );
3289
-
3290
- // Set warning flag but give a much longer grace period for TLS connections
3291
- record.inactivityWarningIssued = true;
3292
-
3293
- // For TLS connections, extend the last activity time considerably
3294
- // This gives browsers more time to re-establish the connection properly
3295
- record.lastActivity = now - this.settings.inactivityTimeout! / 3;
3296
-
3297
- // Try to stimulate the connection with a probe packet
3298
- if (record.outgoing && !record.outgoing.destroyed) {
3299
- try {
3300
- // For TLS connections, send a proper TLS heartbeat-like packet
3301
- // This is just a small empty buffer that won't affect the TLS session
3302
- record.outgoing.write(Buffer.alloc(0));
3303
-
3304
- if (this.settings.enableDetailedLogging) {
3305
- console.log(`[${id}] Sent TLS keep-alive probe packet`);
3306
- }
3307
- } catch (err) {
3308
- console.log(`[${id}] Error sending TLS probe packet: ${err}`);
3309
- }
3310
- }
3311
-
3312
- // Don't proceed to the normal inactivity check logic
3313
- continue;
3314
- }
3315
- }
3316
-
1713
+
3317
1714
  // Use extended timeout for extended-treatment keep-alive connections
3318
1715
  let effectiveTimeout = this.settings.inactivityTimeout!;
3319
1716
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
3320
1717
  const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
3321
1718
  effectiveTimeout = effectiveTimeout * multiplier;
3322
1719
  }
3323
-
1720
+
3324
1721
  if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
3325
1722
  // For keep-alive connections, issue a warning first
3326
1723
  if (record.hasKeepAlive && !record.inactivityWarningIssued) {
3327
1724
  console.log(
3328
- `[${id}] Warning: Keep-alive connection from ${
3329
- record.remoteIP
3330
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
3331
- `Will close in 10 minutes if no activity.`
1725
+ `[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
1726
+ `Will close in 10 minutes if no activity.`
3332
1727
  );
3333
-
1728
+
3334
1729
  // Set warning flag and add grace period
3335
1730
  record.inactivityWarningIssued = true;
3336
1731
  record.lastActivity = now - (effectiveTimeout - 600000);
3337
-
1732
+
3338
1733
  // Try to stimulate activity with a probe packet
3339
1734
  if (record.outgoing && !record.outgoing.destroyed) {
3340
1735
  try {
3341
1736
  record.outgoing.write(Buffer.alloc(0));
3342
-
1737
+
3343
1738
  if (this.settings.enableDetailedLogging) {
3344
1739
  console.log(`[${id}] Sent probe packet to test keep-alive connection`);
3345
1740
  }
@@ -3348,48 +1743,18 @@ export class PortProxy {
3348
1743
  }
3349
1744
  }
3350
1745
  } else {
3351
- // MODIFIED: For TLS connections, be more lenient before closing
3352
- // For TLS browser connections, we need to handle certificate context properly
3353
- if (record.isTLS && record.hasKeepAlive) {
3354
- // For very long inactivity, it's better to close the connection
3355
- // so the browser establishes a new one with a fresh certificate context
3356
- if (inactivityTime > 6 * 60 * 60 * 1000) {
3357
- // 6 hours
3358
- console.log(
3359
- `[${id}] TLS keep-alive connection from ${
3360
- record.remoteIP
3361
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
3362
- `Closing to ensure proper certificate handling on browser reconnect.`
3363
- );
3364
- this.cleanupConnection(record, 'tls_certificate_refresh');
3365
- } else {
3366
- // For shorter inactivity periods, add grace period
3367
- console.log(
3368
- `[${id}] TLS keep-alive connection from ${
3369
- record.remoteIP
3370
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
3371
- `Adding extra grace period.`
3372
- );
3373
-
3374
- // Give additional time for browsers to reconnect properly
3375
- record.lastActivity = now - effectiveTimeout / 2;
3376
- }
3377
- } else {
3378
- // For non-keep-alive or after warning, close the connection
3379
- console.log(
3380
- `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
3381
- `for ${plugins.prettyMs(inactivityTime)}.` +
3382
- (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
3383
- );
3384
- this.cleanupConnection(record, 'inactivity');
3385
- }
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');
3386
1753
  }
3387
1754
  } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
3388
1755
  // If activity detected after warning, clear the warning
3389
1756
  if (this.settings.enableDetailedLogging) {
3390
- console.log(
3391
- `[${id}] Connection activity detected after inactivity warning, resetting warning`
3392
- );
1757
+ console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`);
3393
1758
  }
3394
1759
  record.inactivityWarningIssued = false;
3395
1760
  }
@@ -3438,9 +1803,6 @@ export class PortProxy {
3438
1803
  public async stop() {
3439
1804
  console.log('PortProxy shutting down...');
3440
1805
  this.isShuttingDown = true;
3441
-
3442
- // Stop the session cleanup timer
3443
- stopSessionCleanupTimer();
3444
1806
 
3445
1807
  // Stop accepting new connections
3446
1808
  const closeServerPromises: Promise<void>[] = this.netServers.map(
@@ -3541,4 +1903,4 @@ export class PortProxy {
3541
1903
 
3542
1904
  console.log('PortProxy shutdown complete.');
3543
1905
  }
3544
- }
1906
+ }