@push.rocks/smartproxy 3.31.2 → 3.32.1

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.
@@ -56,6 +56,19 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
56
56
 
57
57
  // NetworkProxy integration
58
58
  networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
59
+
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)
64
+
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
+ };
59
72
  }
60
73
 
61
74
  /**
@@ -87,6 +100,8 @@ interface IConnectionRecord {
87
100
 
88
101
  // Keep-alive tracking
89
102
  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
90
105
  inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
91
106
  incomingTerminationReason?: string | null; // Reason for incoming termination
92
107
  outgoingTerminationReason?: string | null; // Reason for outgoing termination
@@ -108,49 +123,254 @@ interface ITlsSessionInfo {
108
123
  sessionId?: Buffer; // The TLS session ID (if available)
109
124
  ticketId?: string; // Session ticket identifier for newer TLS versions
110
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
111
128
  }
112
129
 
113
- // Global cache of TLS session IDs to SNI domains
114
- // This ensures resumed sessions maintain their SNI binding
115
- const tlsSessionCache = new Map<string, ITlsSessionInfo>();
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
+ }
116
139
 
117
- // Reference to session cleanup timer so we can clear it
118
- let tlsSessionCleanupTimer: NodeJS.Timeout | null = null;
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
+ };
119
147
 
120
- // Start the cleanup timer for session cache
121
- function startSessionCleanupTimer() {
122
- // Avoid creating multiple timers
123
- if (tlsSessionCleanupTimer) {
124
- clearInterval(tlsSessionCleanupTimer);
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();
125
165
  }
126
-
127
- // Create new cleanup timer
128
- tlsSessionCleanupTimer = setInterval(() => {
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
+
129
316
  const now = Date.now();
130
- const expiryTime = 24 * 60 * 60 * 1000; // 24 hours
317
+ let expiredCount = 0;
131
318
 
132
- for (const [sessionId, info] of tlsSessionCache.entries()) {
133
- if (now - info.ticketTimestamp > expiryTime) {
134
- tlsSessionCache.delete(sessionId);
319
+ for (const [key, info] of this.cache.entries()) {
320
+ if (now - info.ticketTimestamp > this.config.expiryTime) {
321
+ this.cache.delete(key);
322
+ expiredCount++;
135
323
  }
136
324
  }
137
- }, 60 * 60 * 1000); // Clean up once per hour
138
-
139
- // Make sure the interval doesn't keep the process alive
140
- if (tlsSessionCleanupTimer.unref) {
141
- tlsSessionCleanupTimer.unref();
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
+ }
142
365
  }
143
366
  }
144
367
 
145
- // Start the timer initially
146
- startSessionCleanupTimer();
368
+ // Create the global session cache
369
+ const tlsSessionCache = new TlsSessionCache();
147
370
 
148
- // Function to stop the cleanup timer (used during shutdown)
371
+ // Legacy function for backward compatibility
149
372
  function stopSessionCleanupTimer() {
150
- if (tlsSessionCleanupTimer) {
151
- clearInterval(tlsSessionCleanupTimer);
152
- tlsSessionCleanupTimer = null;
153
- }
373
+ tlsSessionCache.stop();
154
374
  }
155
375
 
156
376
  /**
@@ -165,6 +385,8 @@ interface ISNIExtractResult {
165
385
  isResumption: boolean; // Whether this appears to be a session resumption
166
386
  resumedDomain?: string; // The domain associated with the session if resuming
167
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
168
390
  }
169
391
 
170
392
  /**
@@ -172,6 +394,11 @@ interface ISNIExtractResult {
172
394
  * Enhanced for robustness and detailed logging.
173
395
  * Also extracts and tracks TLS Session IDs for session resumption handling.
174
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
+ *
175
402
  * @param buffer - Buffer containing the TLS ClientHello.
176
403
  * @param enableLogging - Whether to enable detailed logging.
177
404
  * @returns An object containing SNI and session information, or undefined if parsing fails.
@@ -181,49 +408,159 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
181
408
  // Check if buffer is too small for TLS
182
409
  if (buffer.length < 5) {
183
410
  if (enableLogging) console.log('Buffer too small for TLS header');
184
- return undefined;
411
+ return {
412
+ isResumption: false,
413
+ partialExtract: true // Indicating we need more data
414
+ };
185
415
  }
186
416
 
187
- // Check record type (has to be handshake - 22)
417
+ // Check first record type (has to be handshake - 22)
188
418
  const recordType = buffer.readUInt8(0);
189
419
  if (recordType !== 22) {
190
420
  if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
191
421
  return undefined;
192
422
  }
193
423
 
194
- // Check TLS version (has to be 3.1 or higher)
195
- const majorVersion = buffer.readUInt8(1);
196
- const minorVersion = buffer.readUInt8(2);
197
- if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
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;
198
429
 
199
- // Check record length
200
- const recordLength = buffer.readUInt16BE(3);
201
- if (buffer.length < 5 + recordLength) {
202
- if (enableLogging)
203
- console.log(
204
- `Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`
205
- );
206
- return undefined;
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
+ };
207
514
  }
515
+
516
+ return result;
517
+ } catch (err) {
518
+ console.log(`Error extracting SNI: ${err}`);
519
+ return undefined;
520
+ }
521
+ }
208
522
 
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
209
530
  let offset = 5;
210
- const handshakeType = buffer.readUInt8(offset);
211
- if (handshakeType !== 1) {
531
+
532
+ // Verify this is a handshake message
533
+ const handshakeType = recordBuffer.readUInt8(offset);
534
+ if (handshakeType !== 1) { // 1 = ClientHello
212
535
  if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
213
536
  return undefined;
214
537
  }
215
-
216
- offset += 4; // Skip handshake header (type + length)
217
-
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
+
218
548
  // Client version
219
- const clientMajorVersion = buffer.readUInt8(offset);
220
- const clientMinorVersion = buffer.readUInt8(offset + 1);
549
+ const clientMajorVersion = recordBuffer.readUInt8(offset);
550
+ const clientMinorVersion = recordBuffer.readUInt8(offset + 1);
221
551
  if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
222
-
223
- offset += 2 + 32; // Skip client version and random
224
-
552
+
553
+ // Skip version and random (2 + 32 bytes)
554
+ offset += 2 + 32;
555
+
556
+ // 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
+
225
562
  // Extract Session ID for session resumption tracking
226
- const sessionIDLength = buffer.readUInt8(offset);
563
+ const sessionIDLength = recordBuffer.readUInt8(offset);
227
564
  if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
228
565
 
229
566
  // If there's a session ID, extract it
@@ -233,7 +570,12 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
233
570
  let resumedDomain: string | undefined;
234
571
 
235
572
  if (sessionIDLength > 0) {
236
- sessionId = Buffer.from(buffer.slice(offset + 1, offset + 1 + sessionIDLength));
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));
237
579
 
238
580
  // Convert sessionId to a string key for our cache
239
581
  sessionIdKey = sessionId.toString('hex');
@@ -254,56 +596,121 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
254
596
  }
255
597
  }
256
598
 
257
- offset += 1 + sessionIDLength; // Skip session ID
258
-
599
+ offset += 1 + sessionIDLength; // Skip session ID length and data
600
+
259
601
  // Cipher suites
260
- if (offset + 2 > buffer.length) {
602
+ if (offset + 2 > recordBuffer.length) {
261
603
  if (enableLogging) console.log('Buffer too small for cipher suites length');
262
604
  return undefined;
263
605
  }
264
- const cipherSuitesLength = buffer.readUInt16BE(offset);
606
+
607
+ const cipherSuitesLength = recordBuffer.readUInt16BE(offset);
265
608
  if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
266
- offset += 2 + cipherSuitesLength; // Skip cipher suites
267
-
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
+
268
617
  // Compression methods
269
- if (offset + 1 > buffer.length) {
618
+ if (offset + 1 > recordBuffer.length) {
270
619
  if (enableLogging) console.log('Buffer too small for compression methods length');
271
620
  return undefined;
272
621
  }
273
- const compressionMethodsLength = buffer.readUInt8(offset);
622
+
623
+ const compressionMethodsLength = recordBuffer.readUInt8(offset);
274
624
  if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
275
- offset += 1 + compressionMethodsLength; // Skip compression methods
276
-
277
- // Extensions
278
- if (offset + 2 > buffer.length) {
279
- if (enableLogging) console.log('Buffer too small for extensions length');
625
+
626
+ if (offset + 1 + compressionMethodsLength > recordBuffer.length) {
627
+ if (enableLogging) console.log('Buffer too small for compression methods data');
280
628
  return undefined;
281
629
  }
282
- const extensionsLength = buffer.readUInt16BE(offset);
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);
283
658
  if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
659
+
284
660
  offset += 2;
285
661
  const extensionsEnd = offset + extensionsLength;
286
-
287
- if (extensionsEnd > buffer.length) {
288
- if (enableLogging)
289
- console.log(
290
- `Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`
291
- );
292
- return undefined;
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
+ };
293
686
  }
294
-
687
+
295
688
  // Variables to track session tickets
296
689
  let hasSessionTicket = false;
297
690
  let sessionTicketId: string | undefined;
298
691
 
299
692
  // Parse extensions
300
693
  while (offset + 4 <= extensionsEnd) {
301
- const extensionType = buffer.readUInt16BE(offset);
302
- const extensionLength = buffer.readUInt16BE(offset + 2);
303
-
304
- if (enableLogging)
694
+ const extensionType = recordBuffer.readUInt16BE(offset);
695
+ const extensionLength = recordBuffer.readUInt16BE(offset + 2);
696
+
697
+ if (enableLogging) {
305
698
  console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
306
-
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
+
307
714
  offset += 4;
308
715
 
309
716
  // Check for Session Ticket extension (type 0x0023)
@@ -312,7 +719,7 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
312
719
 
313
720
  // Extract a hash of the ticket for tracking
314
721
  if (extensionLength > 16) { // Ensure we have enough bytes to create a meaningful ID
315
- const ticketBytes = buffer.slice(offset, offset + Math.min(16, extensionLength));
722
+ const ticketBytes = recordBuffer.slice(offset, offset + Math.min(16, extensionLength));
316
723
  sessionTicketId = ticketBytes.toString('hex');
317
724
 
318
725
  if (enableLogging) {
@@ -332,47 +739,74 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
332
739
  }
333
740
  }
334
741
  }
335
-
742
+
743
+ // Server Name Indication extension (type 0x0000)
336
744
  if (extensionType === 0x0000) {
337
- // SNI extension
338
- if (offset + 2 > buffer.length) {
745
+ if (offset + 2 > recordBuffer.length) {
339
746
  if (enableLogging) console.log('Buffer too small for SNI list length');
340
- return undefined;
747
+ return {
748
+ isResumption,
749
+ sessionId,
750
+ sessionIdKey,
751
+ hasSessionTicket,
752
+ partialExtract: true
753
+ };
341
754
  }
342
-
343
- const sniListLength = buffer.readUInt16BE(offset);
755
+
756
+ const sniListLength = recordBuffer.readUInt16BE(offset);
344
757
  if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
758
+
345
759
  offset += 2;
346
760
  const sniListEnd = offset + sniListLength;
347
-
348
- if (sniListEnd > buffer.length) {
349
- if (enableLogging)
350
- console.log(
351
- `Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`
352
- );
353
- return undefined;
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
+ };
354
773
  }
355
-
774
+
356
775
  while (offset + 3 < sniListEnd) {
357
- const nameType = buffer.readUInt8(offset++);
358
- const nameLen = buffer.readUInt16BE(offset);
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);
359
790
  offset += 2;
360
-
791
+
361
792
  if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
362
-
793
+
794
+ // Only process hostname entries (type 0)
363
795
  if (nameType === 0) {
364
- // host_name
365
- if (offset + nameLen > buffer.length) {
366
- if (enableLogging)
367
- console.log(
368
- `Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${
369
- buffer.length
370
- }`
371
- );
372
- return undefined;
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
+ };
373
807
  }
374
-
375
- const serverName = buffer.toString('utf8', offset, offset + nameLen);
808
+
809
+ const serverName = recordBuffer.toString('utf8', offset, offset + nameLen);
376
810
  if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
377
811
 
378
812
  // Store the session ID to domain mapping for future resumptions
@@ -412,15 +846,20 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
412
846
  hasSessionTicket
413
847
  };
414
848
  }
415
-
849
+
850
+ // Skip this name entry
416
851
  offset += nameLen;
417
852
  }
853
+
854
+ // Finished processing the SNI extension without finding a hostname
418
855
  break;
419
856
  } else {
857
+ // Skip other extensions
420
858
  offset += extensionLength;
421
859
  }
422
860
  }
423
-
861
+
862
+ // We finished processing all extensions without finding SNI
424
863
  if (enableLogging) console.log('No SNI extension found');
425
864
 
426
865
  // Even without SNI, we might be dealing with a session resumption
@@ -446,7 +885,7 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
446
885
  resumedDomain
447
886
  };
448
887
  } catch (err) {
449
- console.log(`Error extracting SNI: ${err}`);
888
+ console.log(`Error in extractSNIFromRecord: ${err}`);
450
889
  return undefined;
451
890
  }
452
891
  }
@@ -564,45 +1003,111 @@ export class PortProxy {
564
1003
  private networkProxies: NetworkProxy[] = [];
565
1004
 
566
1005
  constructor(settingsArg: IPortProxySettings) {
567
- // Set hardcoded sensible defaults for all settings
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
568
1057
  this.settings = {
569
1058
  ...settingsArg,
570
- targetIP: settingsArg.targetIP || 'localhost',
571
-
572
- // Hardcoded timeout settings optimized for TLS safety in all deployment scenarios
573
- initialDataTimeout: 60000, // 60 seconds for initial handshake
574
- socketTimeout: 1800000, // 30 minutes - short enough for regular certificate refresh
575
- inactivityCheckInterval: 60000, // 60 seconds interval for regular cleanup
576
- maxConnectionLifetime: 3600000, // 1 hour maximum lifetime for all connections
577
- inactivityTimeout: 1800000, // 30 minutes inactivity timeout
1059
+ targetIP: targetIP,
578
1060
 
579
- gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
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
580
1074
 
581
1075
  // Socket optimization settings
582
1076
  noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
583
1077
  keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
584
- keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
585
- maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
1078
+ keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds (increased)
1079
+ maxPendingDataSize: settingsArg.maxPendingDataSize || 20 * 1024 * 1024, // 20MB to handle large TLS handshakes
586
1080
 
587
1081
  // Feature flags - simplified with sensible defaults
588
- disableInactivityCheck: false, // Always enable inactivity checks for TLS safety
589
- enableKeepAliveProbes: true, // Always enable keep-alive probes for connection health
1082
+ disableInactivityCheck: false, // Still enable inactivity checks
1083
+ enableKeepAliveProbes: true, // Still enable keep-alive probes
590
1084
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
591
1085
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
592
1086
  enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
593
1087
 
594
1088
  // Rate limiting defaults
595
- maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
596
- connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
597
-
598
- // Keep-alive settings with sensible defaults that ensure certificate safety
599
- keepAliveTreatment: 'standard', // Always use standard treatment for certificate safety
600
- keepAliveInactivityMultiplier: 2, // 2x normal inactivity timeout for minimal extension
601
- extendedKeepAliveLifetime: 3 * 60 * 60 * 1000, // 3 hours maximum (previously was 7 days!)
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
602
1099
  };
603
1100
 
604
1101
  // Store NetworkProxy instances if provided
605
1102
  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
+ }
606
1111
  }
607
1112
 
608
1113
  /**
@@ -652,10 +1157,13 @@ export class PortProxy {
652
1157
  );
653
1158
  }
654
1159
 
655
- // Create a connection to the NetworkProxy
1160
+ // Create a connection to the NetworkProxy with optimized settings for reliability
656
1161
  const proxySocket = plugins.net.connect({
657
1162
  host: proxyHost,
658
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
659
1167
  });
660
1168
 
661
1169
  // Store the outgoing socket in the record
@@ -663,6 +1171,30 @@ export class PortProxy {
663
1171
  record.outgoingStartTime = Date.now();
664
1172
  record.usingNetworkProxy = true;
665
1173
  record.networkProxyIndex = proxyIndex;
1174
+
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
+ }
666
1198
 
667
1199
  // Set up error handlers
668
1200
  proxySocket.on('error', (err) => {
@@ -711,6 +1243,41 @@ export class PortProxy {
711
1243
 
712
1244
  // Update activity on data transfer from the proxy socket
713
1245
  proxySocket.on('data', () => this.updateActivity(record));
1246
+
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
+ }
714
1281
 
715
1282
  if (this.settings.enableDetailedLogging) {
716
1283
  console.log(
@@ -832,17 +1399,25 @@ export class PortProxy {
832
1399
 
833
1400
  // Apply keep-alive settings to the outgoing connection as well
834
1401
  if (this.settings.keepAlive) {
835
- targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
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;
1406
+
1407
+ console.log(`[${connectionId}] Keep-alive enabled on outgoing connection with initial delay: ${outgoingInitialDelay}ms`);
836
1408
 
837
1409
  // Apply enhanced TCP keep-alive options if enabled
838
1410
  if (this.settings.enableKeepAliveProbes) {
839
1411
  try {
840
1412
  if ('setKeepAliveProbes' in targetSocket) {
841
- (targetSocket as any).setKeepAliveProbes(10);
1413
+ (targetSocket as any).setKeepAliveProbes(10); // Same probes as incoming
842
1414
  }
843
1415
  if ('setKeepAliveInterval' in targetSocket) {
844
- (targetSocket as any).setKeepAliveInterval(1000);
1416
+ // Use a shorter interval on outgoing for more reliable detection
1417
+ (targetSocket as any).setKeepAliveInterval(800); // Slightly faster than incoming
845
1418
  }
1419
+
1420
+ console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on outgoing connection`);
846
1421
  } catch (err) {
847
1422
  // Ignore errors - these are optional enhancements
848
1423
  if (this.settings.enableDetailedLogging) {
@@ -852,6 +1427,43 @@ export class PortProxy {
852
1427
  }
853
1428
  }
854
1429
  }
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
+ }
855
1467
  }
856
1468
 
857
1469
  // Setup specific error handler for connection phase with enhanced retries
@@ -1040,15 +1652,37 @@ export class PortProxy {
1040
1652
 
1041
1653
  // Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
1042
1654
  if (serverName && record.isTLS) {
1043
- // This listener handles TLS renegotiation detection
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
1044
1659
  socket.on('data', (renegChunk) => {
1045
- if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
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
+
1046
1664
  // Always update activity timestamp for any handshake packet
1047
1665
  this.updateActivity(record);
1048
1666
 
1049
1667
  try {
1668
+ // Enhanced logging for renegotiation
1669
+ console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
1670
+
1050
1671
  // Extract all TLS information including session resumption data
1051
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
+
1052
1686
  let newSNI = sniInfo?.serverName;
1053
1687
 
1054
1688
  // Handle session resumption - if we recognize the session ID, we know what domain it belongs to
@@ -1057,10 +1691,17 @@ export class PortProxy {
1057
1691
  newSNI = sniInfo.resumedDomain;
1058
1692
  }
1059
1693
 
1060
- // IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through
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
1061
1696
  if (newSNI === undefined) {
1062
- console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
1063
- return;
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
1064
1705
  }
1065
1706
 
1066
1707
  // Check if the SNI has changed
@@ -1103,15 +1744,34 @@ export class PortProxy {
1103
1744
  } else {
1104
1745
  console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
1105
1746
  this.initiateCleanupOnce(record, 'sni_mismatch');
1747
+ return;
1106
1748
  }
1107
1749
  } else {
1108
1750
  console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
1109
1751
  }
1110
1752
  } catch (err) {
1111
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);
1112
1760
  }
1113
1761
  }
1114
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
+ });
1115
1775
  }
1116
1776
 
1117
1777
  // Now set up piping for future data and resume the socket
@@ -1149,15 +1809,37 @@ export class PortProxy {
1149
1809
  } else {
1150
1810
  // Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
1151
1811
  if (serverName && record.isTLS) {
1152
- // This listener handles TLS renegotiation detection
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
1153
1816
  socket.on('data', (renegChunk) => {
1154
- if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
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
+
1155
1821
  // Always update activity timestamp for any handshake packet
1156
1822
  this.updateActivity(record);
1157
1823
 
1158
1824
  try {
1825
+ // Enhanced logging for renegotiation
1826
+ console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
1827
+
1159
1828
  // Extract all TLS information including session resumption data
1160
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
+
1161
1843
  let newSNI = sniInfo?.serverName;
1162
1844
 
1163
1845
  // Handle session resumption - if we recognize the session ID, we know what domain it belongs to
@@ -1166,10 +1848,17 @@ export class PortProxy {
1166
1848
  newSNI = sniInfo.resumedDomain;
1167
1849
  }
1168
1850
 
1169
- // IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through
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
1170
1853
  if (newSNI === undefined) {
1171
- console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
1172
- return;
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
1173
1862
  }
1174
1863
 
1175
1864
  // Check if the SNI has changed
@@ -1243,15 +1932,45 @@ export class PortProxy {
1243
1932
  } else {
1244
1933
  console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
1245
1934
  this.initiateCleanupOnce(record, 'sni_mismatch');
1935
+ return;
1246
1936
  }
1247
1937
  } else {
1248
1938
  console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
1249
1939
  }
1250
1940
  } catch (err) {
1251
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);
1252
1948
  }
1253
1949
  }
1254
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
+ });
1255
1974
  }
1256
1975
 
1257
1976
  // Now set up piping
@@ -1461,7 +2180,8 @@ export class PortProxy {
1461
2180
  }
1462
2181
 
1463
2182
  /**
1464
- * Update connection activity timestamp with sleep detection
2183
+ * Update connection activity timestamp with enhanced sleep detection
2184
+ * Improved for chained proxy scenarios and more aggressive handling of stale connections
1465
2185
  */
1466
2186
  private updateActivity(record: IConnectionRecord): void {
1467
2187
  // Get the current time
@@ -1471,62 +2191,108 @@ export class PortProxy {
1471
2191
  if (record.lastActivity > 0) {
1472
2192
  const timeDiff = now - record.lastActivity;
1473
2193
 
1474
- // If time difference is very large (> 30 minutes) and this is a keep-alive connection,
1475
- // this might indicate system sleep rather than just inactivity
1476
- if (timeDiff > 30 * 60 * 1000 && record.hasKeepAlive) {
1477
- if (this.settings.enableDetailedLogging) {
1478
- console.log(
1479
- `[${record.id}] Detected possible system sleep for ${plugins.prettyMs(timeDiff)}. ` +
1480
- `Handling keep-alive connection after long inactivity.`
1481
- );
1482
- }
1483
-
1484
- // For TLS keep-alive connections after sleep/long inactivity, force close
1485
- // to make browser establish a new connection with fresh certificate context
1486
- if (record.isTLS && record.tlsHandshakeComplete) {
1487
- // More generous timeout now that we've fixed the renegotiation handling
1488
- if (timeDiff > 2 * 60 * 60 * 1000) {
1489
- // If inactive for more than 2 hours (increased from 20 minutes)
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) {
1490
2213
  console.log(
1491
- `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
1492
- `Closing to force new connection with fresh certificate.`
2214
+ `[${record.id}] Detected possible short inactivity for ${plugins.prettyMs(timeDiff)}. ` +
2215
+ `Monitoring for TLS connection health.`
1493
2216
  );
1494
- return this.initiateCleanupOnce(record, 'certificate_refresh_needed');
1495
- } else if (timeDiff > 30 * 60 * 1000) {
1496
- // For shorter but still significant inactivity (30+ minutes), refresh TLS state
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) {
1497
2241
  console.log(
1498
- `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
1499
- `Refreshing TLS state.`
2242
+ `[${record.id}] TLS connection in chained proxy inactive for ${plugins.prettyMs(timeDiff)}. ` +
2243
+ `Closing to prevent certificate inconsistencies across chain.`
1500
2244
  );
1501
- this.refreshTlsStateAfterSleep(record);
1502
-
1503
- // Add an additional check in 15 minutes if no activity
1504
- const refreshCheckId = record.id;
1505
- const refreshCheck = setTimeout(() => {
1506
- const currentRecord = this.connectionRecords.get(refreshCheckId);
1507
- if (currentRecord && Date.now() - currentRecord.lastActivity > 15 * 60 * 1000) {
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) {
1508
2265
  console.log(
1509
- `[${refreshCheckId}] No activity detected after TLS refresh. ` +
1510
- `Closing connection to ensure certificate freshness.`
2266
+ `[${refreshCheckId}] No activity detected after TLS refresh (${plugins.prettyMs(verificationTimeDiff)}). ` +
2267
+ `Closing connection to ensure proper browser reconnection.`
1511
2268
  );
1512
2269
  this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
1513
2270
  }
1514
- }, 15 * 60 * 1000);
1515
-
1516
- // Make sure timeout doesn't keep the process alive
1517
- if (refreshCheck.unref) {
1518
- refreshCheck.unref();
1519
2271
  }
1520
- } else {
1521
- // For shorter inactivity periods, try to refresh the TLS state normally
1522
- this.refreshTlsStateAfterSleep(record);
2272
+ }, verificationTimeout);
2273
+
2274
+ // Make sure timeout doesn't keep the process alive
2275
+ if (refreshCheck.unref) {
2276
+ refreshCheck.unref();
1523
2277
  }
1524
2278
  }
1525
2279
 
1526
- // Mark that we detected sleep
2280
+ // Update sleep detection markers
1527
2281
  record.possibleSystemSleep = true;
1528
2282
  record.lastSleepDetection = now;
1529
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
+ }
1530
2296
  }
1531
2297
 
1532
2298
  // Update the activity timestamp
@@ -1539,9 +2305,11 @@ export class PortProxy {
1539
2305
  }
1540
2306
 
1541
2307
  /**
1542
- * Refresh TLS state after sleep detection
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
1543
2311
  */
1544
- private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
2312
+ private performDeepTlsRefresh(record: IConnectionRecord): void {
1545
2313
  // Skip if we're using a NetworkProxy as it handles its own TLS state
1546
2314
  if (record.usingNetworkProxy) {
1547
2315
  return;
@@ -1554,31 +2322,76 @@ export class PortProxy {
1554
2322
  const connectionAge = Date.now() - record.incomingStartTime;
1555
2323
  const hourInMs = 60 * 60 * 1000;
1556
2324
 
1557
- // For TLS browser connections, use a more generous timeout now that
1558
- // we've fixed the renegotiation handling issues
1559
- if (record.isTLS && record.hasKeepAlive && connectionAge > 8 * hourInMs) { // 8 hours instead of 45 minutes
2325
+ // For very long-lived connections, just close them
2326
+ if (connectionAge > 4 * hourInMs) { // Reduced from 8 hours to 4 hours for chained proxies
1560
2327
  console.log(
1561
2328
  `[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
1562
- `Closing to ensure proper certificate handling on browser reconnect in proxy chain.`
2329
+ `Closing to ensure proper certificate handling across proxy chain.`
1563
2330
  );
1564
- return this.initiateCleanupOnce(record, 'certificate_context_refresh');
2331
+ return this.initiateCleanupOnce(record, 'certificate_age_refresh');
1565
2332
  }
1566
2333
 
1567
- // For newer connections, try to send a refresh packet
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
1568
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);
1569
2375
 
1570
2376
  if (this.settings.enableDetailedLogging) {
1571
- console.log(`[${record.id}] Sent refresh packet after sleep detection`);
2377
+ console.log(`[${record.id}] Initiated deep TLS refresh sequence`);
1572
2378
  }
1573
2379
  }
1574
2380
  } catch (err) {
1575
- console.log(`[${record.id}] Error refreshing TLS state: ${err}`);
2381
+ console.log(`[${record.id}] Error starting TLS state refresh: ${err}`);
1576
2382
 
1577
2383
  // If we hit an error, it's likely the connection is already broken
1578
2384
  // Force cleanup to ensure browser reconnects cleanly
1579
2385
  return this.initiateCleanupOnce(record, 'tls_refresh_error');
1580
2386
  }
1581
2387
  }
2388
+
2389
+ /**
2390
+ * Legacy refresh method for backward compatibility
2391
+ */
2392
+ private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
2393
+ return this.performDeepTlsRefresh(record);
2394
+ }
1582
2395
 
1583
2396
  /**
1584
2397
  * Cleans up a connection record.
@@ -1872,12 +2685,20 @@ export class PortProxy {
1872
2685
 
1873
2686
  // Initialize sleep detection fields
1874
2687
  possibleSystemSleep: false,
2688
+
2689
+ // Track keep-alive state for both sides of the connection
2690
+ incomingKeepAliveEnabled: false,
2691
+ outgoingKeepAliveEnabled: false,
1875
2692
  };
1876
2693
 
1877
2694
  // Apply keep-alive settings if enabled
1878
2695
  if (this.settings.keepAlive) {
2696
+ // Configure incoming socket keep-alive
1879
2697
  socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
1880
2698
  connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
2699
+ connectionRecord.incomingKeepAliveEnabled = true;
2700
+
2701
+ console.log(`[${connectionId}] Keep-alive enabled on incoming connection with initial delay: ${this.settings.keepAliveInitialDelay}ms`);
1881
2702
 
1882
2703
  // Apply enhanced TCP keep-alive options if enabled
1883
2704
  if (this.settings.enableKeepAliveProbes) {
@@ -1889,6 +2710,8 @@ export class PortProxy {
1889
2710
  if ('setKeepAliveInterval' in socket) {
1890
2711
  (socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
1891
2712
  }
2713
+
2714
+ console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on incoming connection`);
1892
2715
  } catch (err) {
1893
2716
  // Ignore errors - these are optional enhancements
1894
2717
  if (this.settings.enableDetailedLogging) {