@push.rocks/smartproxy 3.31.1 → 3.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  /**
@@ -108,49 +121,254 @@ interface ITlsSessionInfo {
108
121
  sessionId?: Buffer; // The TLS session ID (if available)
109
122
  ticketId?: string; // Session ticket identifier for newer TLS versions
110
123
  ticketTimestamp: number; // When this session was recorded
124
+ lastAccessed?: number; // When this session was last accessed
125
+ accessCount?: number; // How many times this session has been used
111
126
  }
112
127
 
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>();
128
+ /**
129
+ * Configuration for TLS session cache
130
+ */
131
+ interface ITlsSessionCacheConfig {
132
+ maxEntries: number; // Maximum number of entries to keep in the cache
133
+ expiryTime: number; // Time in ms before sessions expire (default: 24 hours)
134
+ cleanupInterval: number; // Interval in ms to run cleanup (default: 10 minutes)
135
+ enabled: boolean; // Whether session caching is enabled
136
+ }
116
137
 
117
- // Reference to session cleanup timer so we can clear it
118
- let tlsSessionCleanupTimer: NodeJS.Timeout | null = null;
138
+ // Default configuration for session cache
139
+ const DEFAULT_SESSION_CACHE_CONFIG: ITlsSessionCacheConfig = {
140
+ maxEntries: 10000, // Default max 10,000 entries
141
+ expiryTime: 24 * 60 * 60 * 1000, // 24 hours default
142
+ cleanupInterval: 10 * 60 * 1000, // Clean up every 10 minutes
143
+ enabled: true // Enabled by default
144
+ };
145
+
146
+ // Enhanced TLS session cache with size limits and better performance
147
+ class TlsSessionCache {
148
+ private cache = new Map<string, ITlsSessionInfo>();
149
+ private config: ITlsSessionCacheConfig;
150
+ private cleanupTimer: NodeJS.Timeout | null = null;
151
+ private lastCleanupTime: number = 0;
152
+ private cacheStats = {
153
+ hits: 0,
154
+ misses: 0,
155
+ expirations: 0,
156
+ evictions: 0,
157
+ total: 0
158
+ };
119
159
 
120
- // Start the cleanup timer for session cache
121
- function startSessionCleanupTimer() {
122
- // Avoid creating multiple timers
123
- if (tlsSessionCleanupTimer) {
124
- clearInterval(tlsSessionCleanupTimer);
160
+ constructor(config?: Partial<ITlsSessionCacheConfig>) {
161
+ this.config = { ...DEFAULT_SESSION_CACHE_CONFIG, ...config };
162
+ this.startCleanupTimer();
125
163
  }
126
-
127
- // Create new cleanup timer
128
- tlsSessionCleanupTimer = setInterval(() => {
164
+
165
+ /**
166
+ * Get a session from the cache
167
+ */
168
+ public get(key: string): ITlsSessionInfo | undefined {
169
+ // Skip if cache is disabled
170
+ if (!this.config.enabled) return undefined;
171
+
172
+ const entry = this.cache.get(key);
173
+
174
+ if (entry) {
175
+ // Update access information
176
+ entry.lastAccessed = Date.now();
177
+ entry.accessCount = (entry.accessCount || 0) + 1;
178
+ this.cache.set(key, entry);
179
+ this.cacheStats.hits++;
180
+ return entry;
181
+ }
182
+
183
+ this.cacheStats.misses++;
184
+ return undefined;
185
+ }
186
+
187
+ /**
188
+ * Check if the cache has a key
189
+ */
190
+ public has(key: string): boolean {
191
+ // Skip if cache is disabled
192
+ if (!this.config.enabled) return false;
193
+
194
+ const exists = this.cache.has(key);
195
+ if (exists) {
196
+ const entry = this.cache.get(key)!;
197
+
198
+ // Check if entry has expired
199
+ if (Date.now() - entry.ticketTimestamp > this.config.expiryTime) {
200
+ this.cache.delete(key);
201
+ this.cacheStats.expirations++;
202
+ return false;
203
+ }
204
+
205
+ // Update last accessed time
206
+ entry.lastAccessed = Date.now();
207
+ this.cache.set(key, entry);
208
+ }
209
+
210
+ return exists;
211
+ }
212
+
213
+ /**
214
+ * Set a session in the cache
215
+ */
216
+ public set(key: string, value: ITlsSessionInfo): void {
217
+ // Skip if cache is disabled
218
+ if (!this.config.enabled) return;
219
+
220
+ // Ensure timestamps are set
221
+ const entry = {
222
+ ...value,
223
+ lastAccessed: Date.now(),
224
+ accessCount: 0
225
+ };
226
+
227
+ // Check if we need to evict entries
228
+ if (!this.cache.has(key) && this.cache.size >= this.config.maxEntries) {
229
+ this.evictOldest();
230
+ }
231
+
232
+ this.cache.set(key, entry);
233
+ this.cacheStats.total = this.cache.size;
234
+
235
+ // Run cleanup if it's been a while
236
+ const timeSinceCleanup = Date.now() - this.lastCleanupTime;
237
+ if (timeSinceCleanup > this.config.cleanupInterval * 2) {
238
+ this.cleanup();
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Delete a session from the cache
244
+ */
245
+ public delete(key: string): boolean {
246
+ return this.cache.delete(key);
247
+ }
248
+
249
+ /**
250
+ * Clear the entire cache
251
+ */
252
+ public clear(): void {
253
+ this.cache.clear();
254
+ this.cacheStats.total = 0;
255
+ }
256
+
257
+ /**
258
+ * Get cache statistics
259
+ */
260
+ public getStats(): any {
261
+ return {
262
+ ...this.cacheStats,
263
+ size: this.cache.size,
264
+ enabled: this.config.enabled,
265
+ maxEntries: this.config.maxEntries,
266
+ expiryTimeHours: this.config.expiryTime / (60 * 60 * 1000)
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Update cache configuration
272
+ */
273
+ public updateConfig(config: Partial<ITlsSessionCacheConfig>): void {
274
+ this.config = { ...this.config, ...config };
275
+
276
+ // Restart the cleanup timer with new interval
277
+ this.startCleanupTimer();
278
+
279
+ // Run immediate cleanup if max entries was reduced
280
+ if (config.maxEntries && this.cache.size > config.maxEntries) {
281
+ while (this.cache.size > config.maxEntries) {
282
+ this.evictOldest();
283
+ }
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Start the cleanup timer
289
+ */
290
+ private startCleanupTimer(): void {
291
+ if (this.cleanupTimer) {
292
+ clearInterval(this.cleanupTimer);
293
+ this.cleanupTimer = null;
294
+ }
295
+
296
+ if (!this.config.enabled) return;
297
+
298
+ this.cleanupTimer = setInterval(() => {
299
+ this.cleanup();
300
+ }, this.config.cleanupInterval);
301
+
302
+ // Make sure the interval doesn't keep the process alive
303
+ if (this.cleanupTimer.unref) {
304
+ this.cleanupTimer.unref();
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Clean up expired entries
310
+ */
311
+ private cleanup(): void {
312
+ this.lastCleanupTime = Date.now();
313
+
129
314
  const now = Date.now();
130
- const expiryTime = 24 * 60 * 60 * 1000; // 24 hours
315
+ let expiredCount = 0;
131
316
 
132
- for (const [sessionId, info] of tlsSessionCache.entries()) {
133
- if (now - info.ticketTimestamp > expiryTime) {
134
- tlsSessionCache.delete(sessionId);
317
+ for (const [key, info] of this.cache.entries()) {
318
+ if (now - info.ticketTimestamp > this.config.expiryTime) {
319
+ this.cache.delete(key);
320
+ expiredCount++;
135
321
  }
136
322
  }
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();
323
+
324
+ if (expiredCount > 0) {
325
+ this.cacheStats.expirations += expiredCount;
326
+ this.cacheStats.total = this.cache.size;
327
+ console.log(`TLS Session Cache: Cleaned up ${expiredCount} expired entries. ${this.cache.size} entries remaining.`);
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Evict the oldest entries when cache is full
333
+ */
334
+ private evictOldest(): void {
335
+ if (this.cache.size === 0) return;
336
+
337
+ let oldestKey: string | null = null;
338
+ let oldestTime = Date.now();
339
+
340
+ // Strategy: Find least recently accessed entry
341
+ for (const [key, info] of this.cache.entries()) {
342
+ const lastAccess = info.lastAccessed || info.ticketTimestamp;
343
+ if (lastAccess < oldestTime) {
344
+ oldestTime = lastAccess;
345
+ oldestKey = key;
346
+ }
347
+ }
348
+
349
+ if (oldestKey) {
350
+ this.cache.delete(oldestKey);
351
+ this.cacheStats.evictions++;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Stop cleanup timer (used during shutdown)
357
+ */
358
+ public stop(): void {
359
+ if (this.cleanupTimer) {
360
+ clearInterval(this.cleanupTimer);
361
+ this.cleanupTimer = null;
362
+ }
142
363
  }
143
364
  }
144
365
 
145
- // Start the timer initially
146
- startSessionCleanupTimer();
366
+ // Create the global session cache
367
+ const tlsSessionCache = new TlsSessionCache();
147
368
 
148
- // Function to stop the cleanup timer (used during shutdown)
369
+ // Legacy function for backward compatibility
149
370
  function stopSessionCleanupTimer() {
150
- if (tlsSessionCleanupTimer) {
151
- clearInterval(tlsSessionCleanupTimer);
152
- tlsSessionCleanupTimer = null;
153
- }
371
+ tlsSessionCache.stop();
154
372
  }
155
373
 
156
374
  /**
@@ -165,6 +383,8 @@ interface ISNIExtractResult {
165
383
  isResumption: boolean; // Whether this appears to be a session resumption
166
384
  resumedDomain?: string; // The domain associated with the session if resuming
167
385
  partialExtract?: boolean; // Whether this was only a partial extraction (more data needed)
386
+ recordsExamined?: number; // Number of TLS records examined in the buffer
387
+ multipleRecords?: boolean; // Whether multiple TLS records were found in the buffer
168
388
  }
169
389
 
170
390
  /**
@@ -172,6 +392,11 @@ interface ISNIExtractResult {
172
392
  * Enhanced for robustness and detailed logging.
173
393
  * Also extracts and tracks TLS Session IDs for session resumption handling.
174
394
  *
395
+ * Improved to handle:
396
+ * - Multiple TLS records in a single buffer
397
+ * - Fragmented TLS handshakes across multiple records
398
+ * - Partial TLS records that may continue in future chunks
399
+ *
175
400
  * @param buffer - Buffer containing the TLS ClientHello.
176
401
  * @param enableLogging - Whether to enable detailed logging.
177
402
  * @returns An object containing SNI and session information, or undefined if parsing fails.
@@ -181,49 +406,159 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
181
406
  // Check if buffer is too small for TLS
182
407
  if (buffer.length < 5) {
183
408
  if (enableLogging) console.log('Buffer too small for TLS header');
184
- return undefined;
409
+ return {
410
+ isResumption: false,
411
+ partialExtract: true // Indicating we need more data
412
+ };
185
413
  }
186
414
 
187
- // Check record type (has to be handshake - 22)
415
+ // Check first record type (has to be handshake - 22)
188
416
  const recordType = buffer.readUInt8(0);
189
417
  if (recordType !== 22) {
190
418
  if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
191
419
  return undefined;
192
420
  }
193
421
 
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}`);
422
+ // Track multiple records and total records examined
423
+ let recordsExamined = 0;
424
+ let multipleRecords = false;
425
+ let currentPosition = 0;
426
+ let result: ISNIExtractResult | undefined;
198
427
 
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;
428
+ // Process potentially multiple TLS records in the buffer
429
+ while (currentPosition + 5 <= buffer.length) {
430
+ recordsExamined++;
431
+
432
+ // Read record header
433
+ const currentRecordType = buffer.readUInt8(currentPosition);
434
+
435
+ // Only process handshake records (type 22)
436
+ if (currentRecordType !== 22) {
437
+ if (enableLogging) console.log(`Skipping non-handshake record at position ${currentPosition}, type: ${currentRecordType}`);
438
+
439
+ // Move to next potential record
440
+ if (currentPosition + 5 <= buffer.length) {
441
+ // Need at least 5 bytes to determine next record length
442
+ const nextRecordLength = buffer.readUInt16BE(currentPosition + 3);
443
+ currentPosition += 5 + nextRecordLength;
444
+ multipleRecords = true;
445
+ continue;
446
+ } else {
447
+ // Not enough data to determine next record
448
+ break;
449
+ }
450
+ }
451
+
452
+ // Check TLS version
453
+ const majorVersion = buffer.readUInt8(currentPosition + 1);
454
+ const minorVersion = buffer.readUInt8(currentPosition + 2);
455
+ if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion} at position ${currentPosition}`);
456
+
457
+ // Get record length
458
+ const recordLength = buffer.readUInt16BE(currentPosition + 3);
459
+
460
+ // Check if we have the complete record
461
+ if (currentPosition + 5 + recordLength > buffer.length) {
462
+ if (enableLogging) {
463
+ console.log(`Incomplete TLS record at position ${currentPosition}. Expected: ${currentPosition + 5 + recordLength}, Got: ${buffer.length}`);
464
+ }
465
+
466
+ // Return partial info and signal that more data is needed
467
+ return {
468
+ isResumption: false,
469
+ partialExtract: true,
470
+ recordsExamined,
471
+ multipleRecords
472
+ };
473
+ }
474
+
475
+ // Process this record - extract handshake information
476
+ const recordResult = extractSNIFromRecord(
477
+ buffer.slice(currentPosition, currentPosition + 5 + recordLength),
478
+ enableLogging
479
+ );
480
+
481
+ // If we found SNI or session info in this record, store it
482
+ if (recordResult && (recordResult.serverName || recordResult.isResumption)) {
483
+ result = recordResult;
484
+ result.recordsExamined = recordsExamined;
485
+ result.multipleRecords = multipleRecords;
486
+
487
+ // Once we've found SNI or session resumption info, we can stop processing
488
+ // But we'll still set the multipleRecords flag to indicate more records exist
489
+ if (currentPosition + 5 + recordLength < buffer.length) {
490
+ result.multipleRecords = true;
491
+ }
492
+
493
+ break;
494
+ }
495
+
496
+ // Move to the next record
497
+ currentPosition += 5 + recordLength;
498
+
499
+ // Set the flag if we've processed multiple records
500
+ if (currentPosition < buffer.length) {
501
+ multipleRecords = true;
502
+ }
207
503
  }
504
+
505
+ // If we processed records but didn't find SNI or session info
506
+ if (recordsExamined > 0 && !result) {
507
+ return {
508
+ isResumption: false,
509
+ recordsExamined,
510
+ multipleRecords
511
+ };
512
+ }
513
+
514
+ return result;
515
+ } catch (err) {
516
+ console.log(`Error extracting SNI: ${err}`);
517
+ return undefined;
518
+ }
519
+ }
208
520
 
521
+ /**
522
+ * Extracts SNI information from a single TLS record
523
+ * This helper function processes a single complete TLS record
524
+ */
525
+ function extractSNIFromRecord(recordBuffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
526
+ try {
527
+ // Skip the 5-byte TLS record header
209
528
  let offset = 5;
210
- const handshakeType = buffer.readUInt8(offset);
211
- if (handshakeType !== 1) {
529
+
530
+ // Verify this is a handshake message
531
+ const handshakeType = recordBuffer.readUInt8(offset);
532
+ if (handshakeType !== 1) { // 1 = ClientHello
212
533
  if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
213
534
  return undefined;
214
535
  }
215
-
216
- offset += 4; // Skip handshake header (type + length)
217
-
536
+
537
+ // Skip the 4-byte handshake header (type + 3 bytes length)
538
+ offset += 4;
539
+
540
+ // Check if we have at least 38 more bytes for protocol version and random
541
+ if (offset + 38 > recordBuffer.length) {
542
+ if (enableLogging) console.log('Buffer too small for handshake header');
543
+ return undefined;
544
+ }
545
+
218
546
  // Client version
219
- const clientMajorVersion = buffer.readUInt8(offset);
220
- const clientMinorVersion = buffer.readUInt8(offset + 1);
547
+ const clientMajorVersion = recordBuffer.readUInt8(offset);
548
+ const clientMinorVersion = recordBuffer.readUInt8(offset + 1);
221
549
  if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
222
-
223
- offset += 2 + 32; // Skip client version and random
224
-
550
+
551
+ // Skip version and random (2 + 32 bytes)
552
+ offset += 2 + 32;
553
+
554
+ // Session ID
555
+ if (offset + 1 > recordBuffer.length) {
556
+ if (enableLogging) console.log('Buffer too small for session ID length');
557
+ return undefined;
558
+ }
559
+
225
560
  // Extract Session ID for session resumption tracking
226
- const sessionIDLength = buffer.readUInt8(offset);
561
+ const sessionIDLength = recordBuffer.readUInt8(offset);
227
562
  if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
228
563
 
229
564
  // If there's a session ID, extract it
@@ -233,7 +568,12 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
233
568
  let resumedDomain: string | undefined;
234
569
 
235
570
  if (sessionIDLength > 0) {
236
- sessionId = Buffer.from(buffer.slice(offset + 1, offset + 1 + sessionIDLength));
571
+ if (offset + 1 + sessionIDLength > recordBuffer.length) {
572
+ if (enableLogging) console.log('Buffer too small for session ID data');
573
+ return undefined;
574
+ }
575
+
576
+ sessionId = Buffer.from(recordBuffer.slice(offset + 1, offset + 1 + sessionIDLength));
237
577
 
238
578
  // Convert sessionId to a string key for our cache
239
579
  sessionIdKey = sessionId.toString('hex');
@@ -254,56 +594,121 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
254
594
  }
255
595
  }
256
596
 
257
- offset += 1 + sessionIDLength; // Skip session ID
258
-
597
+ offset += 1 + sessionIDLength; // Skip session ID length and data
598
+
259
599
  // Cipher suites
260
- if (offset + 2 > buffer.length) {
600
+ if (offset + 2 > recordBuffer.length) {
261
601
  if (enableLogging) console.log('Buffer too small for cipher suites length');
262
602
  return undefined;
263
603
  }
264
- const cipherSuitesLength = buffer.readUInt16BE(offset);
604
+
605
+ const cipherSuitesLength = recordBuffer.readUInt16BE(offset);
265
606
  if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
266
- offset += 2 + cipherSuitesLength; // Skip cipher suites
267
-
607
+
608
+ if (offset + 2 + cipherSuitesLength > recordBuffer.length) {
609
+ if (enableLogging) console.log('Buffer too small for cipher suites data');
610
+ return undefined;
611
+ }
612
+
613
+ offset += 2 + cipherSuitesLength; // Skip cipher suites length and data
614
+
268
615
  // Compression methods
269
- if (offset + 1 > buffer.length) {
616
+ if (offset + 1 > recordBuffer.length) {
270
617
  if (enableLogging) console.log('Buffer too small for compression methods length');
271
618
  return undefined;
272
619
  }
273
- const compressionMethodsLength = buffer.readUInt8(offset);
620
+
621
+ const compressionMethodsLength = recordBuffer.readUInt8(offset);
274
622
  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');
623
+
624
+ if (offset + 1 + compressionMethodsLength > recordBuffer.length) {
625
+ if (enableLogging) console.log('Buffer too small for compression methods data');
280
626
  return undefined;
281
627
  }
282
- const extensionsLength = buffer.readUInt16BE(offset);
628
+
629
+ offset += 1 + compressionMethodsLength; // Skip compression methods length and data
630
+
631
+ // Check if we have extensions data
632
+ if (offset + 2 > recordBuffer.length) {
633
+ if (enableLogging) console.log('No extensions data found - end of ClientHello');
634
+
635
+ // Even without SNI, we might be dealing with a session resumption
636
+ if (isResumption && resumedDomain) {
637
+ return {
638
+ serverName: resumedDomain, // Use the domain from previous session
639
+ sessionId,
640
+ sessionIdKey,
641
+ hasSessionTicket: false,
642
+ isResumption: true,
643
+ resumedDomain
644
+ };
645
+ }
646
+
647
+ return {
648
+ isResumption,
649
+ sessionId,
650
+ sessionIdKey
651
+ };
652
+ }
653
+
654
+ // Extensions
655
+ const extensionsLength = recordBuffer.readUInt16BE(offset);
283
656
  if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
657
+
284
658
  offset += 2;
285
659
  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;
660
+
661
+ if (extensionsEnd > recordBuffer.length) {
662
+ if (enableLogging) {
663
+ console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${recordBuffer.length}`);
664
+ }
665
+
666
+ // Even without complete extensions, we might be dealing with a session resumption
667
+ if (isResumption && resumedDomain) {
668
+ return {
669
+ serverName: resumedDomain, // Use the domain from previous session
670
+ sessionId,
671
+ sessionIdKey,
672
+ hasSessionTicket: false,
673
+ isResumption: true,
674
+ resumedDomain
675
+ };
676
+ }
677
+
678
+ return {
679
+ isResumption,
680
+ sessionId,
681
+ sessionIdKey,
682
+ partialExtract: true // Indicating we have incomplete extensions data
683
+ };
293
684
  }
294
-
685
+
295
686
  // Variables to track session tickets
296
687
  let hasSessionTicket = false;
297
688
  let sessionTicketId: string | undefined;
298
689
 
299
690
  // Parse extensions
300
691
  while (offset + 4 <= extensionsEnd) {
301
- const extensionType = buffer.readUInt16BE(offset);
302
- const extensionLength = buffer.readUInt16BE(offset + 2);
303
-
304
- if (enableLogging)
692
+ const extensionType = recordBuffer.readUInt16BE(offset);
693
+ const extensionLength = recordBuffer.readUInt16BE(offset + 2);
694
+
695
+ if (enableLogging) {
305
696
  console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
306
-
697
+ }
698
+
699
+ if (offset + 4 + extensionLength > recordBuffer.length) {
700
+ if (enableLogging) {
701
+ console.log(`Extension data incomplete. Expected: ${offset + 4 + extensionLength}, Got: ${recordBuffer.length}`);
702
+ }
703
+ return {
704
+ isResumption,
705
+ sessionId,
706
+ sessionIdKey,
707
+ hasSessionTicket,
708
+ partialExtract: true
709
+ };
710
+ }
711
+
307
712
  offset += 4;
308
713
 
309
714
  // Check for Session Ticket extension (type 0x0023)
@@ -312,7 +717,7 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
312
717
 
313
718
  // Extract a hash of the ticket for tracking
314
719
  if (extensionLength > 16) { // Ensure we have enough bytes to create a meaningful ID
315
- const ticketBytes = buffer.slice(offset, offset + Math.min(16, extensionLength));
720
+ const ticketBytes = recordBuffer.slice(offset, offset + Math.min(16, extensionLength));
316
721
  sessionTicketId = ticketBytes.toString('hex');
317
722
 
318
723
  if (enableLogging) {
@@ -332,47 +737,74 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
332
737
  }
333
738
  }
334
739
  }
335
-
740
+
741
+ // Server Name Indication extension (type 0x0000)
336
742
  if (extensionType === 0x0000) {
337
- // SNI extension
338
- if (offset + 2 > buffer.length) {
743
+ if (offset + 2 > recordBuffer.length) {
339
744
  if (enableLogging) console.log('Buffer too small for SNI list length');
340
- return undefined;
745
+ return {
746
+ isResumption,
747
+ sessionId,
748
+ sessionIdKey,
749
+ hasSessionTicket,
750
+ partialExtract: true
751
+ };
341
752
  }
342
-
343
- const sniListLength = buffer.readUInt16BE(offset);
753
+
754
+ const sniListLength = recordBuffer.readUInt16BE(offset);
344
755
  if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
756
+
345
757
  offset += 2;
346
758
  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;
759
+
760
+ if (sniListEnd > recordBuffer.length) {
761
+ if (enableLogging) {
762
+ console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${recordBuffer.length}`);
763
+ }
764
+ return {
765
+ isResumption,
766
+ sessionId,
767
+ sessionIdKey,
768
+ hasSessionTicket,
769
+ partialExtract: true
770
+ };
354
771
  }
355
-
772
+
356
773
  while (offset + 3 < sniListEnd) {
357
- const nameType = buffer.readUInt8(offset++);
358
- const nameLen = buffer.readUInt16BE(offset);
774
+ const nameType = recordBuffer.readUInt8(offset++);
775
+
776
+ if (offset + 2 > recordBuffer.length) {
777
+ if (enableLogging) console.log('Buffer too small for SNI name length');
778
+ return {
779
+ isResumption,
780
+ sessionId,
781
+ sessionIdKey,
782
+ hasSessionTicket,
783
+ partialExtract: true
784
+ };
785
+ }
786
+
787
+ const nameLen = recordBuffer.readUInt16BE(offset);
359
788
  offset += 2;
360
-
789
+
361
790
  if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
362
-
791
+
792
+ // Only process hostname entries (type 0)
363
793
  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;
794
+ if (offset + nameLen > recordBuffer.length) {
795
+ if (enableLogging) {
796
+ console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${recordBuffer.length}`);
797
+ }
798
+ return {
799
+ isResumption,
800
+ sessionId,
801
+ sessionIdKey,
802
+ hasSessionTicket,
803
+ partialExtract: true
804
+ };
373
805
  }
374
-
375
- const serverName = buffer.toString('utf8', offset, offset + nameLen);
806
+
807
+ const serverName = recordBuffer.toString('utf8', offset, offset + nameLen);
376
808
  if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
377
809
 
378
810
  // Store the session ID to domain mapping for future resumptions
@@ -412,15 +844,20 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
412
844
  hasSessionTicket
413
845
  };
414
846
  }
415
-
847
+
848
+ // Skip this name entry
416
849
  offset += nameLen;
417
850
  }
851
+
852
+ // Finished processing the SNI extension without finding a hostname
418
853
  break;
419
854
  } else {
855
+ // Skip other extensions
420
856
  offset += extensionLength;
421
857
  }
422
858
  }
423
-
859
+
860
+ // We finished processing all extensions without finding SNI
424
861
  if (enableLogging) console.log('No SNI extension found');
425
862
 
426
863
  // Even without SNI, we might be dealing with a session resumption
@@ -446,7 +883,7 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
446
883
  resumedDomain
447
884
  };
448
885
  } catch (err) {
449
- console.log(`Error extracting SNI: ${err}`);
886
+ console.log(`Error in extractSNIFromRecord: ${err}`);
450
887
  return undefined;
451
888
  }
452
889
  }
@@ -564,17 +1001,71 @@ export class PortProxy {
564
1001
  private networkProxies: NetworkProxy[] = [];
565
1002
 
566
1003
  constructor(settingsArg: IPortProxySettings) {
567
- // Set hardcoded sensible defaults for all settings
1004
+ // Auto-detect if this is a chained proxy based on targetIP
1005
+ const targetIP = settingsArg.targetIP || 'localhost';
1006
+ const isChainedProxy = settingsArg.isChainedProxy !== undefined
1007
+ ? settingsArg.isChainedProxy
1008
+ : (targetIP === 'localhost' || targetIP === '127.0.0.1');
1009
+
1010
+ // Use more aggressive timeouts for chained proxies
1011
+ const aggressiveTlsRefresh = settingsArg.aggressiveTlsRefresh !== undefined
1012
+ ? settingsArg.aggressiveTlsRefresh
1013
+ : isChainedProxy;
1014
+
1015
+ // Configure TLS session cache if specified
1016
+ if (settingsArg.tlsSessionCache) {
1017
+ tlsSessionCache.updateConfig({
1018
+ enabled: settingsArg.tlsSessionCache.enabled,
1019
+ maxEntries: settingsArg.tlsSessionCache.maxEntries,
1020
+ expiryTime: settingsArg.tlsSessionCache.expiryTime,
1021
+ cleanupInterval: settingsArg.tlsSessionCache.cleanupInterval
1022
+ });
1023
+
1024
+ console.log(`Configured TLS session cache with custom settings. Current stats: ${JSON.stringify(tlsSessionCache.getStats())}`);
1025
+ }
1026
+
1027
+ // Determine appropriate timeouts based on proxy chain position
1028
+ let socketTimeout = 1800000; // 30 minutes default
1029
+
1030
+ if (isChainedProxy) {
1031
+ // Use shorter timeouts for chained proxies to prevent certificate issues
1032
+ const chainPosition = settingsArg.chainPosition || 'middle';
1033
+
1034
+ // Adjust timeouts based on position in chain
1035
+ switch (chainPosition) {
1036
+ case 'first':
1037
+ // First proxy can be a bit more lenient as it handles browser connections
1038
+ socketTimeout = 1500000; // 25 minutes
1039
+ break;
1040
+ case 'middle':
1041
+ // Middle proxies need shorter timeouts
1042
+ socketTimeout = 1200000; // 20 minutes
1043
+ break;
1044
+ case 'last':
1045
+ // Last proxy directly connects to backend
1046
+ socketTimeout = 1800000; // 30 minutes
1047
+ break;
1048
+ }
1049
+
1050
+ console.log(`Configured as ${chainPosition} proxy in chain. Using adjusted timeouts for optimal TLS handling.`);
1051
+ }
1052
+
1053
+ // Set hardcoded sensible defaults for all settings with chain-aware adjustments
568
1054
  this.settings = {
569
1055
  ...settingsArg,
570
- targetIP: settingsArg.targetIP || 'localhost',
1056
+ targetIP: targetIP,
1057
+
1058
+ // Record the chained proxy status for use in other methods
1059
+ isChainedProxy: isChainedProxy,
1060
+ chainPosition: settingsArg.chainPosition || (isChainedProxy ? 'middle' : 'last'),
1061
+ aggressiveTlsRefresh: aggressiveTlsRefresh,
571
1062
 
572
1063
  // Hardcoded timeout settings optimized for TLS safety in all deployment scenarios
573
1064
  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
1065
+ socketTimeout: socketTimeout, // Adjusted based on chain position
1066
+ inactivityCheckInterval: isChainedProxy ? 30000 : 60000, // More frequent checks for chains
1067
+ maxConnectionLifetime: isChainedProxy ? 2700000 : 3600000, // 45min or 1hr lifetime
1068
+ inactivityTimeout: isChainedProxy ? 1200000 : 1800000, // 20min or 30min inactivity timeout
578
1069
 
579
1070
  gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
580
1071
 
@@ -598,11 +1089,22 @@ export class PortProxy {
598
1089
  // Keep-alive settings with sensible defaults that ensure certificate safety
599
1090
  keepAliveTreatment: 'standard', // Always use standard treatment for certificate safety
600
1091
  keepAliveInactivityMultiplier: 2, // 2x normal inactivity timeout for minimal extension
601
- extendedKeepAliveLifetime: 3 * 60 * 60 * 1000, // 3 hours maximum (previously was 7 days!)
1092
+ // Use shorter lifetime for chained proxies
1093
+ extendedKeepAliveLifetime: isChainedProxy
1094
+ ? 2 * 60 * 60 * 1000 // 2 hours for chained proxies
1095
+ : 3 * 60 * 60 * 1000, // 3 hours for standalone proxies
602
1096
  };
603
1097
 
604
1098
  // Store NetworkProxy instances if provided
605
1099
  this.networkProxies = settingsArg.networkProxies || [];
1100
+
1101
+ // Log proxy configuration details
1102
+ console.log(`PortProxy initialized with ${isChainedProxy ? 'chained proxy' : 'standalone'} configuration.`);
1103
+ if (isChainedProxy) {
1104
+ console.log(`TLS certificate refresh: ${aggressiveTlsRefresh ? 'Aggressive' : 'Standard'}`);
1105
+ console.log(`Connection lifetime: ${plugins.prettyMs(this.settings.maxConnectionLifetime)}`);
1106
+ console.log(`Inactivity timeout: ${plugins.prettyMs(this.settings.inactivityTimeout)}`);
1107
+ }
606
1108
  }
607
1109
 
608
1110
  /**
@@ -1179,15 +1681,46 @@ export class PortProxy {
1179
1681
  // Allow if the new SNI matches existing domain config or find a new matching config
1180
1682
  let allowed = false;
1181
1683
 
1684
+ // First check if the new SNI is allowed under the existing domain config
1685
+ // This is the preferred approach as it maintains the existing connection context
1182
1686
  if (record.domainConfig) {
1183
1687
  allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
1688
+
1689
+ if (allowed) {
1690
+ console.log(`[${connectionId}] Rehandshake SNI ${newSNI} allowed by existing domain config`);
1691
+ }
1184
1692
  }
1185
1693
 
1694
+ // If not allowed by existing config, try to find an alternative domain config
1186
1695
  if (!allowed) {
1187
- const newDomainConfig = this.settings.domainConfigs.find((config) =>
1696
+ // First try exact match
1697
+ let newDomainConfig = this.settings.domainConfigs.find((config) =>
1188
1698
  config.domains.some((d) => plugins.minimatch(newSNI, d))
1189
1699
  );
1190
1700
 
1701
+ // If no exact match, try flexible matching with domain parts (for wildcard domains)
1702
+ if (!newDomainConfig) {
1703
+ console.log(`[${connectionId}] No exact domain config match for rehandshake SNI: ${newSNI}, trying flexible matching`);
1704
+
1705
+ const domainParts = newSNI.split('.');
1706
+
1707
+ // Try matching with parent domains or wildcard patterns
1708
+ if (domainParts.length > 2) {
1709
+ const parentDomain = domainParts.slice(1).join('.');
1710
+ const wildcardDomain = '*.' + parentDomain;
1711
+
1712
+ console.log(`[${connectionId}] Trying alternative patterns: ${parentDomain} or ${wildcardDomain}`);
1713
+
1714
+ newDomainConfig = this.settings.domainConfigs.find((config) =>
1715
+ config.domains.some((d) =>
1716
+ d === parentDomain ||
1717
+ d === wildcardDomain ||
1718
+ plugins.minimatch(parentDomain, d)
1719
+ )
1720
+ );
1721
+ }
1722
+ }
1723
+
1191
1724
  if (newDomainConfig) {
1192
1725
  const effectiveAllowedIPs = [
1193
1726
  ...newDomainConfig.allowedIPs,
@@ -1430,7 +1963,8 @@ export class PortProxy {
1430
1963
  }
1431
1964
 
1432
1965
  /**
1433
- * Update connection activity timestamp with sleep detection
1966
+ * Update connection activity timestamp with enhanced sleep detection
1967
+ * Improved for chained proxy scenarios and more aggressive handling of stale connections
1434
1968
  */
1435
1969
  private updateActivity(record: IConnectionRecord): void {
1436
1970
  // Get the current time
@@ -1440,62 +1974,107 @@ export class PortProxy {
1440
1974
  if (record.lastActivity > 0) {
1441
1975
  const timeDiff = now - record.lastActivity;
1442
1976
 
1443
- // If time difference is very large (> 30 minutes) and this is a keep-alive connection,
1444
- // this might indicate system sleep rather than just inactivity
1445
- if (timeDiff > 30 * 60 * 1000 && record.hasKeepAlive) {
1446
- if (this.settings.enableDetailedLogging) {
1447
- console.log(
1448
- `[${record.id}] Detected possible system sleep for ${plugins.prettyMs(timeDiff)}. ` +
1449
- `Handling keep-alive connection after long inactivity.`
1450
- );
1451
- }
1452
-
1453
- // For TLS keep-alive connections after sleep/long inactivity, force close
1454
- // to make browser establish a new connection with fresh certificate context
1455
- if (record.isTLS && record.tlsHandshakeComplete) {
1456
- // More generous timeout now that we've fixed the renegotiation handling
1457
- if (timeDiff > 2 * 60 * 60 * 1000) {
1458
- // If inactive for more than 2 hours (increased from 20 minutes)
1977
+ // Enhanced sleep detection with graduated thresholds
1978
+ // For chained proxies, we need to be more aggressive about refreshing connections
1979
+ const isChainedProxy = this.settings.targetIP === 'localhost' || this.settings.targetIP === '127.0.0.1';
1980
+ const minuteInMs = 60 * 1000;
1981
+
1982
+ // Different thresholds based on connection type and configuration
1983
+ const shortInactivityThreshold = isChainedProxy ? 10 * minuteInMs : 15 * minuteInMs;
1984
+ const mediumInactivityThreshold = isChainedProxy ? 20 * minuteInMs : 30 * minuteInMs;
1985
+ const longInactivityThreshold = isChainedProxy ? 60 * minuteInMs : 120 * minuteInMs;
1986
+
1987
+ // Short inactivity (10-15 mins) - Might be temporary network issue or short sleep
1988
+ if (timeDiff > shortInactivityThreshold) {
1989
+ if (record.isTLS && !record.possibleSystemSleep) {
1990
+ // Record first detection of possible sleep/inactivity
1991
+ record.possibleSystemSleep = true;
1992
+ record.lastSleepDetection = now;
1993
+
1994
+ if (this.settings.enableDetailedLogging) {
1459
1995
  console.log(
1460
- `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
1461
- `Closing to force new connection with fresh certificate.`
1996
+ `[${record.id}] Detected possible short inactivity for ${plugins.prettyMs(timeDiff)}. ` +
1997
+ `Monitoring for TLS connection health.`
1462
1998
  );
1463
- return this.initiateCleanupOnce(record, 'certificate_refresh_needed');
1464
- } else if (timeDiff > 30 * 60 * 1000) {
1465
- // For shorter but still significant inactivity (30+ minutes), refresh TLS state
1999
+ }
2000
+
2001
+ // For TLS connections, send a minimal probe to check connection health
2002
+ if (!record.usingNetworkProxy && record.outgoing && !record.outgoing.destroyed) {
2003
+ try {
2004
+ record.outgoing.write(Buffer.alloc(0));
2005
+ } catch (err) {
2006
+ console.log(`[${record.id}] Error sending TLS probe: ${err}`);
2007
+ }
2008
+ }
2009
+ }
2010
+ }
2011
+
2012
+ // Medium inactivity (20-30 mins) - Likely a sleep event or network change
2013
+ if (timeDiff > mediumInactivityThreshold && record.hasKeepAlive) {
2014
+ console.log(
2015
+ `[${record.id}] Detected medium inactivity period for ${plugins.prettyMs(timeDiff)}. ` +
2016
+ `Taking proactive steps for connection health.`
2017
+ );
2018
+
2019
+ // For TLS connections, we need more aggressive handling
2020
+ if (record.isTLS && record.tlsHandshakeComplete) {
2021
+ // If in a chained proxy, we should be even more aggressive about refreshing
2022
+ if (isChainedProxy) {
1466
2023
  console.log(
1467
- `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
1468
- `Refreshing TLS state.`
2024
+ `[${record.id}] TLS connection in chained proxy inactive for ${plugins.prettyMs(timeDiff)}. ` +
2025
+ `Closing to prevent certificate inconsistencies across chain.`
1469
2026
  );
1470
- this.refreshTlsStateAfterSleep(record);
1471
-
1472
- // Add an additional check in 15 minutes if no activity
1473
- const refreshCheckId = record.id;
1474
- const refreshCheck = setTimeout(() => {
1475
- const currentRecord = this.connectionRecords.get(refreshCheckId);
1476
- if (currentRecord && Date.now() - currentRecord.lastActivity > 15 * 60 * 1000) {
2027
+ return this.initiateCleanupOnce(record, 'chained_proxy_inactivity');
2028
+ }
2029
+
2030
+ // For TLS in single proxy, try refresh first
2031
+ console.log(
2032
+ `[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
2033
+ `Attempting active refresh of TLS state.`
2034
+ );
2035
+
2036
+ // Attempt deep TLS state refresh with buffer flush
2037
+ this.performDeepTlsRefresh(record);
2038
+
2039
+ // Schedule verification check with tighter timing for chained setups
2040
+ const verificationTimeout = isChainedProxy ? 5 * minuteInMs : 10 * minuteInMs;
2041
+ const refreshCheckId = record.id;
2042
+ const refreshCheck = setTimeout(() => {
2043
+ const currentRecord = this.connectionRecords.get(refreshCheckId);
2044
+ if (currentRecord) {
2045
+ const verificationTimeDiff = Date.now() - currentRecord.lastActivity;
2046
+ if (verificationTimeDiff > verificationTimeout / 2) {
1477
2047
  console.log(
1478
- `[${refreshCheckId}] No activity detected after TLS refresh. ` +
1479
- `Closing connection to ensure certificate freshness.`
2048
+ `[${refreshCheckId}] No activity detected after TLS refresh (${plugins.prettyMs(verificationTimeDiff)}). ` +
2049
+ `Closing connection to ensure proper browser reconnection.`
1480
2050
  );
1481
2051
  this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
1482
2052
  }
1483
- }, 15 * 60 * 1000);
1484
-
1485
- // Make sure timeout doesn't keep the process alive
1486
- if (refreshCheck.unref) {
1487
- refreshCheck.unref();
1488
2053
  }
1489
- } else {
1490
- // For shorter inactivity periods, try to refresh the TLS state normally
1491
- this.refreshTlsStateAfterSleep(record);
2054
+ }, verificationTimeout);
2055
+
2056
+ // Make sure timeout doesn't keep the process alive
2057
+ if (refreshCheck.unref) {
2058
+ refreshCheck.unref();
1492
2059
  }
1493
2060
  }
1494
2061
 
1495
- // Mark that we detected sleep
2062
+ // Update sleep detection markers
1496
2063
  record.possibleSystemSleep = true;
1497
2064
  record.lastSleepDetection = now;
1498
2065
  }
2066
+
2067
+ // Long inactivity (60-120 mins) - Definite sleep/suspend or major network change
2068
+ if (timeDiff > longInactivityThreshold) {
2069
+ console.log(
2070
+ `[${record.id}] Detected long inactivity period of ${plugins.prettyMs(timeDiff)}. ` +
2071
+ `Closing connection to ensure fresh certificate context.`
2072
+ );
2073
+
2074
+ // For long periods, we always want to force close and let browser reconnect
2075
+ // This ensures fresh certificates and proper TLS context across the chain
2076
+ return this.initiateCleanupOnce(record, 'extended_inactivity_refresh');
2077
+ }
1499
2078
  }
1500
2079
 
1501
2080
  // Update the activity timestamp
@@ -1508,9 +2087,11 @@ export class PortProxy {
1508
2087
  }
1509
2088
 
1510
2089
  /**
1511
- * Refresh TLS state after sleep detection
2090
+ * Perform deep TLS state refresh after sleep detection
2091
+ * More aggressive than the standard refresh, specifically designed for
2092
+ * recovering connections after system sleep in chained proxy setups
1512
2093
  */
1513
- private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
2094
+ private performDeepTlsRefresh(record: IConnectionRecord): void {
1514
2095
  // Skip if we're using a NetworkProxy as it handles its own TLS state
1515
2096
  if (record.usingNetworkProxy) {
1516
2097
  return;
@@ -1523,31 +2104,76 @@ export class PortProxy {
1523
2104
  const connectionAge = Date.now() - record.incomingStartTime;
1524
2105
  const hourInMs = 60 * 60 * 1000;
1525
2106
 
1526
- // For TLS browser connections, use a more generous timeout now that
1527
- // we've fixed the renegotiation handling issues
1528
- if (record.isTLS && record.hasKeepAlive && connectionAge > 8 * hourInMs) { // 8 hours instead of 45 minutes
2107
+ // For very long-lived connections, just close them
2108
+ if (connectionAge > 4 * hourInMs) { // Reduced from 8 hours to 4 hours for chained proxies
1529
2109
  console.log(
1530
2110
  `[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
1531
- `Closing to ensure proper certificate handling on browser reconnect in proxy chain.`
2111
+ `Closing to ensure proper certificate handling across proxy chain.`
1532
2112
  );
1533
- return this.initiateCleanupOnce(record, 'certificate_context_refresh');
2113
+ return this.initiateCleanupOnce(record, 'certificate_age_refresh');
1534
2114
  }
1535
2115
 
1536
- // For newer connections, try to send a refresh packet
2116
+ // Perform a series of actions to try to refresh the TLS state
2117
+
2118
+ // 1. Send a zero-length buffer to trigger any pending errors
1537
2119
  record.outgoing.write(Buffer.alloc(0));
2120
+
2121
+ // 2. Check socket state
2122
+ if (record.outgoing.writableEnded || !record.outgoing.writable) {
2123
+ console.log(`[${record.id}] Socket no longer writable during refresh`);
2124
+ return this.initiateCleanupOnce(record, 'socket_state_error');
2125
+ }
2126
+
2127
+ // 3. For TLS connections, try to force background renegotiation
2128
+ // by manipulating socket timeouts
2129
+ const originalTimeout = record.outgoing.timeout;
2130
+ record.outgoing.setTimeout(100); // Set very short timeout
2131
+
2132
+ // 4. Create a small delay to allow timeout to process
2133
+ setTimeout(() => {
2134
+ try {
2135
+ if (record.outgoing && !record.outgoing.destroyed) {
2136
+ // Reset timeout to original value
2137
+ record.outgoing.setTimeout(originalTimeout || 0);
2138
+
2139
+ // Send another probe with random data (16 bytes) that will be ignored by TLS layer
2140
+ // but might trigger internal state updates in the TLS implementation
2141
+ const probeBuffer = Buffer.alloc(16);
2142
+ // Fill with random data
2143
+ for (let i = 0; i < 16; i++) {
2144
+ probeBuffer[i] = Math.floor(Math.random() * 256);
2145
+ }
2146
+ record.outgoing.write(Buffer.alloc(0));
2147
+
2148
+ if (this.settings.enableDetailedLogging) {
2149
+ console.log(`[${record.id}] Completed deep TLS refresh sequence`);
2150
+ }
2151
+ }
2152
+ } catch (innerErr) {
2153
+ console.log(`[${record.id}] Error during deep TLS refresh: ${innerErr}`);
2154
+ this.initiateCleanupOnce(record, 'deep_refresh_error');
2155
+ }
2156
+ }, 150);
1538
2157
 
1539
2158
  if (this.settings.enableDetailedLogging) {
1540
- console.log(`[${record.id}] Sent refresh packet after sleep detection`);
2159
+ console.log(`[${record.id}] Initiated deep TLS refresh sequence`);
1541
2160
  }
1542
2161
  }
1543
2162
  } catch (err) {
1544
- console.log(`[${record.id}] Error refreshing TLS state: ${err}`);
2163
+ console.log(`[${record.id}] Error starting TLS state refresh: ${err}`);
1545
2164
 
1546
2165
  // If we hit an error, it's likely the connection is already broken
1547
2166
  // Force cleanup to ensure browser reconnects cleanly
1548
2167
  return this.initiateCleanupOnce(record, 'tls_refresh_error');
1549
2168
  }
1550
2169
  }
2170
+
2171
+ /**
2172
+ * Legacy refresh method for backward compatibility
2173
+ */
2174
+ private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
2175
+ return this.performDeepTlsRefresh(record);
2176
+ }
1551
2177
 
1552
2178
  /**
1553
2179
  * Cleans up a connection record.
@@ -2002,15 +2628,63 @@ export class PortProxy {
2002
2628
  // The resumed domain will be in serverName if this is a session resumption
2003
2629
  if (serverName && connectionRecord.lockedDomain === serverName && serverName !== '') {
2004
2630
  // Override domain config lookup for session resumption - crucial for certificate selection
2005
- const resumedDomainConfig = this.settings.domainConfigs.find((config) =>
2631
+
2632
+ // First try an exact match
2633
+ let resumedDomainConfig = this.settings.domainConfigs.find((config) =>
2006
2634
  config.domains.some((d) => plugins.minimatch(serverName, d))
2007
2635
  );
2008
2636
 
2637
+ // If no exact match found, try a more flexible approach using domain parts
2638
+ if (!resumedDomainConfig) {
2639
+ console.log(`[${connectionId}] No exact domain config match for resumed domain: ${serverName}, trying flexible matching`);
2640
+
2641
+ // Extract domain parts (e.g., for "sub.example.com" try matching with "*.example.com")
2642
+ const domainParts = serverName.split('.');
2643
+
2644
+ // Try matching with parent domains or wildcard patterns
2645
+ if (domainParts.length > 2) {
2646
+ const parentDomain = domainParts.slice(1).join('.');
2647
+ const wildcardDomain = '*.' + parentDomain;
2648
+
2649
+ console.log(`[${connectionId}] Trying alternative patterns: ${parentDomain} or ${wildcardDomain}`);
2650
+
2651
+ resumedDomainConfig = this.settings.domainConfigs.find((config) =>
2652
+ config.domains.some((d) =>
2653
+ d === parentDomain ||
2654
+ d === wildcardDomain ||
2655
+ plugins.minimatch(parentDomain, d)
2656
+ )
2657
+ );
2658
+ }
2659
+ }
2660
+
2009
2661
  if (resumedDomainConfig) {
2010
2662
  domainConfig = resumedDomainConfig;
2011
- console.log(`[${connectionId}] Using domain config for resumed session: ${serverName}`);
2663
+ console.log(`[${connectionId}] Found domain config for resumed session: ${serverName} -> ${resumedDomainConfig.domains.join(',')}`);
2012
2664
  } else {
2013
- console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
2665
+ // As a fallback, use the first domain config with the same target IP if possible
2666
+ if (domainConfig && domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
2667
+ const targetIP = domainConfig.targetIPs[0];
2668
+
2669
+ const similarConfig = this.settings.domainConfigs.find((config) =>
2670
+ config.targetIPs && config.targetIPs.includes(targetIP)
2671
+ );
2672
+
2673
+ if (similarConfig && similarConfig !== domainConfig) {
2674
+ console.log(`[${connectionId}] Using similar domain config with matching target IP for resumed domain: ${serverName}`);
2675
+ domainConfig = similarConfig;
2676
+ } else {
2677
+ console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
2678
+ // Log available domains to help diagnose the issue
2679
+ console.log(`[${connectionId}] Available domains:`,
2680
+ this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
2681
+ }
2682
+ } else {
2683
+ console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
2684
+ // Log available domains to help diagnose the issue
2685
+ console.log(`[${connectionId}] Available domains:`,
2686
+ this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
2687
+ }
2014
2688
  }
2015
2689
  }
2016
2690