@push.rocks/smartproxy 3.32.0 → 3.32.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,18 +10,13 @@ export interface IDomainConfig {
10
10
  portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
11
11
  // Allow domain-specific timeout override
12
12
  connectionTimeout?: number; // Connection timeout override (ms)
13
-
13
+
14
14
  // New properties for NetworkProxy integration
15
15
  useNetworkProxy?: boolean; // When true, forwards TLS connections to NetworkProxy
16
16
  networkProxyIndex?: number; // Optional index to specify which NetworkProxy to use (defaults to 0)
17
17
  }
18
18
 
19
- /**
20
- * Port proxy settings including global allowed port ranges
21
- *
22
- * NOTE: In version 3.31.0+, timeout settings have been simplified and hardcoded with sensible defaults
23
- * to ensure TLS certificate safety in all deployment scenarios, especially chained proxies.
24
- */
19
+ /** Port proxy settings including global allowed port ranges */
25
20
  export interface IPortProxySettings extends plugins.tls.TlsOptions {
26
21
  fromPort: number;
27
22
  toPort: number;
@@ -32,10 +27,14 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
32
27
  defaultBlockedIPs?: string[];
33
28
  preserveSourceIP?: boolean;
34
29
 
35
- // Simplified timeout settings
30
+ // Timeout settings
31
+ initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
32
+ socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
33
+ inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
34
+ maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h)
35
+ inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
36
+
36
37
  gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
37
-
38
- // Ranged port settings
39
38
  globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
40
39
  forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
41
40
 
@@ -45,7 +44,9 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
45
44
  keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
46
45
  maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
47
46
 
48
- // Logging settings
47
+ // Enhanced features
48
+ disableInactivityCheck?: boolean; // Disable inactivity checking entirely
49
+ enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
49
50
  enableDetailedLogging?: boolean; // Enable detailed connection logging
50
51
  enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
51
52
  enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
@@ -53,22 +54,14 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
53
54
  // Rate limiting and security
54
55
  maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
55
56
  connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
56
-
57
- // NetworkProxy integration
58
- networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
59
57
 
60
- // New settings for chained proxy configurations and TLS handling
61
- isChainedProxy?: boolean; // Whether this proxy is part of a proxy chain (detected automatically if unspecified)
62
- chainPosition?: 'first' | 'middle' | 'last'; // Position in the proxy chain (affects TLS handling)
63
- aggressiveTlsRefresh?: boolean; // Use more aggressive TLS refresh timeouts (default: true for chained)
58
+ // Enhanced keep-alive settings
59
+ keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
60
+ keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
61
+ extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
64
62
 
65
- // TLS session cache configuration
66
- tlsSessionCache?: {
67
- enabled?: boolean; // Whether to use the TLS session cache (default: true)
68
- maxEntries?: number; // Maximum cache entries (default: 10000)
69
- expiryTime?: number; // Session expiry time in ms (default: 24h)
70
- cleanupInterval?: number; // Cache cleanup interval in ms (default: 10min)
71
- };
63
+ // New property for NetworkProxy integration
64
+ networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
72
65
  }
73
66
 
74
67
  /**
@@ -97,808 +90,182 @@ interface IConnectionRecord {
97
90
  tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
98
91
  hasReceivedInitialData: boolean; // Whether initial data has been received
99
92
  domainConfig?: IDomainConfig; // Associated domain config for this connection
100
-
93
+
101
94
  // Keep-alive tracking
102
95
  hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
103
96
  inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
104
97
  incomingTerminationReason?: string | null; // Reason for incoming termination
105
98
  outgoingTerminationReason?: string | null; // Reason for outgoing termination
106
-
99
+
107
100
  // New field for NetworkProxy tracking
108
101
  usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
109
102
  networkProxyIndex?: number; // Which NetworkProxy instance is being used
110
-
111
- // Sleep detection fields
112
- possibleSystemSleep?: boolean; // Flag to indicate a possible system sleep was detected
113
- lastSleepDetection?: number; // Timestamp of the last sleep detection
114
- }
115
-
116
- /**
117
- * Structure to track TLS session information for proper resumption handling
118
- */
119
- interface ITlsSessionInfo {
120
- domain: string; // The SNI domain associated with this session
121
- sessionId?: Buffer; // The TLS session ID (if available)
122
- ticketId?: string; // Session ticket identifier for newer TLS versions
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
126
- }
127
-
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
- }
137
-
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
- };
159
-
160
- constructor(config?: Partial<ITlsSessionCacheConfig>) {
161
- this.config = { ...DEFAULT_SESSION_CACHE_CONFIG, ...config };
162
- this.startCleanupTimer();
163
- }
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
-
314
- const now = Date.now();
315
- let expiredCount = 0;
316
-
317
- for (const [key, info] of this.cache.entries()) {
318
- if (now - info.ticketTimestamp > this.config.expiryTime) {
319
- this.cache.delete(key);
320
- expiredCount++;
321
- }
322
- }
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
- }
363
- }
364
- }
365
-
366
- // Create the global session cache
367
- const tlsSessionCache = new TlsSessionCache();
368
-
369
- // Legacy function for backward compatibility
370
- function stopSessionCleanupTimer() {
371
- tlsSessionCache.stop();
372
- }
373
-
374
- /**
375
- * Return type for the extractSNIInfo function
376
- */
377
- interface ISNIExtractResult {
378
- serverName?: string; // The extracted SNI hostname
379
- sessionId?: Buffer; // The TLS session ID if present
380
- sessionIdKey?: string; // The hex string representation of session ID
381
- sessionTicketId?: string; // Session ticket identifier for TLS 1.3+ resumption
382
- hasSessionTicket?: boolean; // Whether a session ticket extension was found
383
- isResumption: boolean; // Whether this appears to be a session resumption
384
- resumedDomain?: string; // The domain associated with the session if resuming
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
388
103
  }
389
104
 
390
105
  /**
391
106
  * Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
392
107
  * Enhanced for robustness and detailed logging.
393
- * Also extracts and tracks TLS Session IDs for session resumption handling.
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
- *
400
108
  * @param buffer - Buffer containing the TLS ClientHello.
401
109
  * @param enableLogging - Whether to enable detailed logging.
402
- * @returns An object containing SNI and session information, or undefined if parsing fails.
110
+ * @returns The server name if found, otherwise undefined.
403
111
  */
404
- function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
112
+ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
405
113
  try {
406
114
  // Check if buffer is too small for TLS
407
115
  if (buffer.length < 5) {
408
116
  if (enableLogging) console.log('Buffer too small for TLS header');
409
- return {
410
- isResumption: false,
411
- partialExtract: true // Indicating we need more data
412
- };
117
+ return undefined;
413
118
  }
414
119
 
415
- // Check first record type (has to be handshake - 22)
120
+ // Check record type (has to be handshake - 22)
416
121
  const recordType = buffer.readUInt8(0);
417
122
  if (recordType !== 22) {
418
123
  if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
419
124
  return undefined;
420
125
  }
421
126
 
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;
127
+ // Check TLS version (has to be 3.1 or higher)
128
+ const majorVersion = buffer.readUInt8(1);
129
+ const minorVersion = buffer.readUInt8(2);
130
+ if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
427
131
 
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
- }
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
- };
132
+ // Check record length
133
+ const recordLength = buffer.readUInt16BE(3);
134
+ if (buffer.length < 5 + recordLength) {
135
+ if (enableLogging)
136
+ console.log(
137
+ `Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`
138
+ );
139
+ return undefined;
512
140
  }
513
-
514
- return result;
515
- } catch (err) {
516
- console.log(`Error extracting SNI: ${err}`);
517
- return undefined;
518
- }
519
- }
520
141
 
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
528
142
  let offset = 5;
529
-
530
- // Verify this is a handshake message
531
- const handshakeType = recordBuffer.readUInt8(offset);
532
- if (handshakeType !== 1) { // 1 = ClientHello
143
+ const handshakeType = buffer.readUInt8(offset);
144
+ if (handshakeType !== 1) {
533
145
  if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
534
146
  return undefined;
535
147
  }
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
-
148
+
149
+ offset += 4; // Skip handshake header (type + length)
150
+
546
151
  // Client version
547
- const clientMajorVersion = recordBuffer.readUInt8(offset);
548
- const clientMinorVersion = recordBuffer.readUInt8(offset + 1);
152
+ const clientMajorVersion = buffer.readUInt8(offset);
153
+ const clientMinorVersion = buffer.readUInt8(offset + 1);
549
154
  if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
550
-
551
- // Skip version and random (2 + 32 bytes)
552
- offset += 2 + 32;
553
-
155
+
156
+ offset += 2 + 32; // Skip client version and random
157
+
554
158
  // 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
-
560
- // Extract Session ID for session resumption tracking
561
- const sessionIDLength = recordBuffer.readUInt8(offset);
159
+ const sessionIDLength = buffer.readUInt8(offset);
562
160
  if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
563
-
564
- // If there's a session ID, extract it
565
- let sessionId: Buffer | undefined;
566
- let sessionIdKey: string | undefined;
567
- let isResumption = false;
568
- let resumedDomain: string | undefined;
569
-
570
- if (sessionIDLength > 0) {
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));
577
-
578
- // Convert sessionId to a string key for our cache
579
- sessionIdKey = sessionId.toString('hex');
580
-
581
- if (enableLogging) {
582
- console.log(`Session ID: ${sessionIdKey}`);
583
- }
584
-
585
- // Check if this is a session resumption attempt
586
- if (tlsSessionCache.has(sessionIdKey)) {
587
- const cachedInfo = tlsSessionCache.get(sessionIdKey)!;
588
- resumedDomain = cachedInfo.domain;
589
- isResumption = true;
590
-
591
- if (enableLogging) {
592
- console.log(`TLS Session Resumption detected for domain: ${resumedDomain}`);
593
- }
594
- }
595
- }
596
-
597
- offset += 1 + sessionIDLength; // Skip session ID length and data
598
-
161
+ offset += 1 + sessionIDLength; // Skip session ID
162
+
599
163
  // Cipher suites
600
- if (offset + 2 > recordBuffer.length) {
164
+ if (offset + 2 > buffer.length) {
601
165
  if (enableLogging) console.log('Buffer too small for cipher suites length');
602
166
  return undefined;
603
167
  }
604
-
605
- const cipherSuitesLength = recordBuffer.readUInt16BE(offset);
168
+ const cipherSuitesLength = buffer.readUInt16BE(offset);
606
169
  if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
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
-
170
+ offset += 2 + cipherSuitesLength; // Skip cipher suites
171
+
615
172
  // Compression methods
616
- if (offset + 1 > recordBuffer.length) {
173
+ if (offset + 1 > buffer.length) {
617
174
  if (enableLogging) console.log('Buffer too small for compression methods length');
618
175
  return undefined;
619
176
  }
620
-
621
- const compressionMethodsLength = recordBuffer.readUInt8(offset);
177
+ const compressionMethodsLength = buffer.readUInt8(offset);
622
178
  if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
623
-
624
- if (offset + 1 + compressionMethodsLength > recordBuffer.length) {
625
- if (enableLogging) console.log('Buffer too small for compression methods data');
179
+ offset += 1 + compressionMethodsLength; // Skip compression methods
180
+
181
+ // Extensions
182
+ if (offset + 2 > buffer.length) {
183
+ if (enableLogging) console.log('Buffer too small for extensions length');
626
184
  return undefined;
627
185
  }
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);
186
+ const extensionsLength = buffer.readUInt16BE(offset);
656
187
  if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
657
-
658
188
  offset += 2;
659
189
  const extensionsEnd = offset + extensionsLength;
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
- };
190
+
191
+ if (extensionsEnd > buffer.length) {
192
+ if (enableLogging)
193
+ console.log(
194
+ `Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`
195
+ );
196
+ return undefined;
684
197
  }
685
-
686
- // Variables to track session tickets
687
- let hasSessionTicket = false;
688
- let sessionTicketId: string | undefined;
689
-
198
+
690
199
  // Parse extensions
691
200
  while (offset + 4 <= extensionsEnd) {
692
- const extensionType = recordBuffer.readUInt16BE(offset);
693
- const extensionLength = recordBuffer.readUInt16BE(offset + 2);
694
-
695
- if (enableLogging) {
201
+ const extensionType = buffer.readUInt16BE(offset);
202
+ const extensionLength = buffer.readUInt16BE(offset + 2);
203
+
204
+ if (enableLogging)
696
205
  console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
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
-
206
+
712
207
  offset += 4;
713
-
714
- // Check for Session Ticket extension (type 0x0023)
715
- if (extensionType === 0x0023 && extensionLength > 0) {
716
- hasSessionTicket = true;
717
-
718
- // Extract a hash of the ticket for tracking
719
- if (extensionLength > 16) { // Ensure we have enough bytes to create a meaningful ID
720
- const ticketBytes = recordBuffer.slice(offset, offset + Math.min(16, extensionLength));
721
- sessionTicketId = ticketBytes.toString('hex');
722
-
723
- if (enableLogging) {
724
- console.log(`Session Ticket found, ID: ${sessionTicketId}`);
725
-
726
- // Check if this is a known session ticket
727
- if (tlsSessionCache.has(`ticket:${sessionTicketId}`)) {
728
- const cachedInfo = tlsSessionCache.get(`ticket:${sessionTicketId}`);
729
- console.log(`TLS Session Ticket Resumption detected for domain: ${cachedInfo?.domain}`);
730
-
731
- // Set isResumption and resumedDomain if not already set
732
- if (!isResumption && !resumedDomain) {
733
- isResumption = true;
734
- resumedDomain = cachedInfo?.domain;
735
- }
736
- }
737
- }
738
- }
739
- }
740
-
741
- // Server Name Indication extension (type 0x0000)
208
+
742
209
  if (extensionType === 0x0000) {
743
- if (offset + 2 > recordBuffer.length) {
210
+ // SNI extension
211
+ if (offset + 2 > buffer.length) {
744
212
  if (enableLogging) console.log('Buffer too small for SNI list length');
745
- return {
746
- isResumption,
747
- sessionId,
748
- sessionIdKey,
749
- hasSessionTicket,
750
- partialExtract: true
751
- };
213
+ return undefined;
752
214
  }
753
-
754
- const sniListLength = recordBuffer.readUInt16BE(offset);
215
+
216
+ const sniListLength = buffer.readUInt16BE(offset);
755
217
  if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
756
-
757
218
  offset += 2;
758
219
  const sniListEnd = offset + sniListLength;
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
- };
220
+
221
+ if (sniListEnd > buffer.length) {
222
+ if (enableLogging)
223
+ console.log(
224
+ `Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`
225
+ );
226
+ return undefined;
771
227
  }
772
-
228
+
773
229
  while (offset + 3 < sniListEnd) {
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);
230
+ const nameType = buffer.readUInt8(offset++);
231
+ const nameLen = buffer.readUInt16BE(offset);
788
232
  offset += 2;
789
-
233
+
790
234
  if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
791
-
792
- // Only process hostname entries (type 0)
235
+
793
236
  if (nameType === 0) {
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
- };
237
+ // host_name
238
+ if (offset + nameLen > buffer.length) {
239
+ if (enableLogging)
240
+ console.log(
241
+ `Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${
242
+ buffer.length
243
+ }`
244
+ );
245
+ return undefined;
805
246
  }
806
-
807
- const serverName = recordBuffer.toString('utf8', offset, offset + nameLen);
247
+
248
+ const serverName = buffer.toString('utf8', offset, offset + nameLen);
808
249
  if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
809
-
810
- // Store the session ID to domain mapping for future resumptions
811
- if (sessionIdKey && sessionId && serverName) {
812
- tlsSessionCache.set(sessionIdKey, {
813
- domain: serverName,
814
- sessionId: sessionId,
815
- ticketTimestamp: Date.now()
816
- });
817
-
818
- if (enableLogging) {
819
- console.log(`Stored session ${sessionIdKey} for domain ${serverName}`);
820
- }
821
- }
822
-
823
- // Also store session ticket information if present
824
- if (sessionTicketId && serverName) {
825
- tlsSessionCache.set(`ticket:${sessionTicketId}`, {
826
- domain: serverName,
827
- ticketId: sessionTicketId,
828
- ticketTimestamp: Date.now()
829
- });
830
-
831
- if (enableLogging) {
832
- console.log(`Stored session ticket ${sessionTicketId} for domain ${serverName}`);
833
- }
834
- }
835
-
836
- // Return the complete extraction result
837
- return {
838
- serverName,
839
- sessionId,
840
- sessionIdKey,
841
- sessionTicketId,
842
- isResumption,
843
- resumedDomain,
844
- hasSessionTicket
845
- };
250
+ return serverName;
846
251
  }
847
-
848
- // Skip this name entry
252
+
849
253
  offset += nameLen;
850
254
  }
851
-
852
- // Finished processing the SNI extension without finding a hostname
853
255
  break;
854
256
  } else {
855
- // Skip other extensions
856
257
  offset += extensionLength;
857
258
  }
858
259
  }
859
-
860
- // We finished processing all extensions without finding SNI
260
+
861
261
  if (enableLogging) console.log('No SNI extension found');
862
-
863
- // Even without SNI, we might be dealing with a session resumption
864
- if (isResumption && resumedDomain) {
865
- return {
866
- serverName: resumedDomain, // Use the domain from previous session
867
- sessionId,
868
- sessionIdKey,
869
- sessionTicketId,
870
- hasSessionTicket,
871
- isResumption: true,
872
- resumedDomain
873
- };
874
- }
875
-
876
- // Return a basic result with just the session info
877
- return {
878
- isResumption,
879
- sessionId,
880
- sessionIdKey,
881
- sessionTicketId,
882
- hasSessionTicket,
883
- resumedDomain
884
- };
262
+ return undefined;
885
263
  } catch (err) {
886
- console.log(`Error in extractSNIFromRecord: ${err}`);
264
+ console.log(`Error extracting SNI: ${err}`);
887
265
  return undefined;
888
266
  }
889
267
  }
890
268
 
891
- /**
892
- * Legacy wrapper for extractSNIInfo to maintain backward compatibility
893
- * @param buffer - Buffer containing the TLS ClientHello
894
- * @param enableLogging - Whether to enable detailed logging
895
- * @returns The server name if found, otherwise undefined
896
- */
897
- function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
898
- const result = extractSNIInfo(buffer, enableLogging);
899
- return result?.serverName;
900
- }
901
-
902
269
  // Helper: Check if a port falls within any of the given port ranges
903
270
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
904
271
  return ranges.some((range) => port >= range.from && port <= range.to);
@@ -961,22 +328,7 @@ const randomizeTimeout = (baseTimeout: number, variationPercent: number = 5): nu
961
328
 
962
329
  export class PortProxy {
963
330
  private netServers: plugins.net.Server[] = [];
964
-
965
- // Define the internal settings interface to include all fields, including those removed from the public interface
966
- settings: IPortProxySettings & {
967
- // Internal fields removed from public interface in 3.31.0+
968
- initialDataTimeout: number;
969
- socketTimeout: number;
970
- inactivityCheckInterval: number;
971
- maxConnectionLifetime: number;
972
- inactivityTimeout: number;
973
- disableInactivityCheck: boolean;
974
- enableKeepAliveProbes: boolean;
975
- keepAliveTreatment: 'standard' | 'extended' | 'immortal';
976
- keepAliveInactivityMultiplier: number;
977
- extendedKeepAliveLifetime: number;
978
- };
979
-
331
+ settings: IPortProxySettings;
980
332
  private connectionRecords: Map<string, IConnectionRecord> = new Map();
981
333
  private connectionLogger: NodeJS.Timeout | null = null;
982
334
  private isShuttingDown: boolean = false;
@@ -996,115 +348,51 @@ export class PortProxy {
996
348
  // Connection tracking by IP for rate limiting
997
349
  private connectionsByIP: Map<string, Set<string>> = new Map();
998
350
  private connectionRateByIP: Map<string, number[]> = new Map();
999
-
351
+
1000
352
  // New property to store NetworkProxy instances
1001
353
  private networkProxies: NetworkProxy[] = [];
1002
354
 
1003
355
  constructor(settingsArg: IPortProxySettings) {
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
356
+ // Set reasonable defaults for all settings
1054
357
  this.settings = {
1055
358
  ...settingsArg,
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,
1062
-
1063
- // Hardcoded timeout settings optimized for TLS safety in all deployment scenarios
1064
- initialDataTimeout: 60000, // 60 seconds for initial handshake
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
1069
-
359
+ targetIP: settingsArg.targetIP || 'localhost',
360
+
361
+ // Timeout settings with reasonable defaults
362
+ initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake
363
+ socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout
364
+ inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
365
+ maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default
366
+ inactivityTimeout: ensureSafeTimeout(settingsArg.inactivityTimeout || 14400000), // 4 hours inactivity timeout
367
+
1070
368
  gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
1071
369
 
1072
370
  // Socket optimization settings
1073
371
  noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
1074
372
  keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
1075
- keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
373
+ keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
1076
374
  maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
1077
375
 
1078
- // Feature flags - simplified with sensible defaults
1079
- disableInactivityCheck: false, // Always enable inactivity checks for TLS safety
1080
- enableKeepAliveProbes: true, // Always enable keep-alive probes for connection health
376
+ // Feature flags
377
+ disableInactivityCheck: settingsArg.disableInactivityCheck || false,
378
+ enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
379
+ ? settingsArg.enableKeepAliveProbes : true, // Enable by default
1081
380
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
1082
381
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
1083
- enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
382
+ enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
1084
383
 
1085
384
  // Rate limiting defaults
1086
385
  maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
1087
386
  connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
1088
-
1089
- // Keep-alive settings with sensible defaults that ensure certificate safety
1090
- keepAliveTreatment: 'standard', // Always use standard treatment for certificate safety
1091
- keepAliveInactivityMultiplier: 2, // 2x normal inactivity timeout for minimal extension
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
387
+
388
+ // Enhanced keep-alive settings
389
+ keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', // Extended by default
390
+ keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, // 6x normal inactivity timeout
391
+ extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
1096
392
  };
1097
-
393
+
1098
394
  // Store NetworkProxy instances if provided
1099
395
  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
- }
1108
396
  }
1109
397
 
1110
398
  /**
@@ -1125,66 +413,58 @@ export class PortProxy {
1125
413
  serverName?: string
1126
414
  ): void {
1127
415
  // Determine which NetworkProxy to use
1128
- const proxyIndex =
1129
- domainConfig.networkProxyIndex !== undefined ? domainConfig.networkProxyIndex : 0;
1130
-
416
+ const proxyIndex = domainConfig.networkProxyIndex !== undefined
417
+ ? domainConfig.networkProxyIndex
418
+ : 0;
419
+
1131
420
  // Validate the NetworkProxy index
1132
421
  if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
1133
- console.log(
1134
- `[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
1135
- );
422
+ console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`);
1136
423
  // Fall back to direct connection
1137
- return this.setupDirectConnection(
1138
- connectionId,
1139
- socket,
1140
- record,
1141
- domainConfig,
1142
- serverName,
1143
- initialData
1144
- );
424
+ return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData);
1145
425
  }
1146
-
426
+
1147
427
  const networkProxy = this.networkProxies[proxyIndex];
1148
428
  const proxyPort = networkProxy.getListeningPort();
1149
429
  const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
1150
-
430
+
1151
431
  if (this.settings.enableDetailedLogging) {
1152
432
  console.log(
1153
433
  `[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
1154
434
  );
1155
435
  }
1156
-
436
+
1157
437
  // Create a connection to the NetworkProxy
1158
438
  const proxySocket = plugins.net.connect({
1159
439
  host: proxyHost,
1160
- port: proxyPort,
440
+ port: proxyPort
1161
441
  });
1162
-
442
+
1163
443
  // Store the outgoing socket in the record
1164
444
  record.outgoing = proxySocket;
1165
445
  record.outgoingStartTime = Date.now();
1166
446
  record.usingNetworkProxy = true;
1167
447
  record.networkProxyIndex = proxyIndex;
1168
-
448
+
1169
449
  // Set up error handlers
1170
450
  proxySocket.on('error', (err) => {
1171
451
  console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
1172
452
  this.cleanupConnection(record, 'network_proxy_connect_error');
1173
453
  });
1174
-
454
+
1175
455
  // Handle connection to NetworkProxy
1176
456
  proxySocket.on('connect', () => {
1177
457
  if (this.settings.enableDetailedLogging) {
1178
458
  console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
1179
459
  }
1180
-
460
+
1181
461
  // First send the initial data that contains the TLS ClientHello
1182
462
  proxySocket.write(initialData);
1183
-
463
+
1184
464
  // Now set up bidirectional piping between client and NetworkProxy
1185
465
  socket.pipe(proxySocket);
1186
466
  proxySocket.pipe(socket);
1187
-
467
+
1188
468
  // Setup cleanup handlers
1189
469
  proxySocket.on('close', () => {
1190
470
  if (this.settings.enableDetailedLogging) {
@@ -1192,28 +472,18 @@ export class PortProxy {
1192
472
  }
1193
473
  this.cleanupConnection(record, 'network_proxy_closed');
1194
474
  });
1195
-
475
+
1196
476
  socket.on('close', () => {
1197
477
  if (this.settings.enableDetailedLogging) {
1198
- console.log(
1199
- `[${connectionId}] Client connection closed after forwarding to NetworkProxy`
1200
- );
478
+ console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`);
1201
479
  }
1202
480
  this.cleanupConnection(record, 'client_closed');
1203
481
  });
1204
482
 
1205
- // Special handler for TLS handshake detection with NetworkProxy
1206
- socket.on('data', (chunk: Buffer) => {
1207
- // Check for TLS handshake packets (ContentType.handshake)
1208
- if (chunk.length > 0 && chunk[0] === 22) {
1209
- console.log(`[${connectionId}] Detected potential TLS handshake with NetworkProxy, updating activity`);
1210
- this.updateActivity(record);
1211
- }
1212
- });
1213
-
1214
- // Update activity on data transfer from the proxy socket
483
+ // Update activity on data transfer
484
+ socket.on('data', () => this.updateActivity(record));
1215
485
  proxySocket.on('data', () => this.updateActivity(record));
1216
-
486
+
1217
487
  if (this.settings.enableDetailedLogging) {
1218
488
  console.log(
1219
489
  `[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
@@ -1221,7 +491,7 @@ export class PortProxy {
1221
491
  }
1222
492
  });
1223
493
  }
1224
-
494
+
1225
495
  /**
1226
496
  * Sets up a direct connection to the target (original behavior)
1227
497
  * This is used when NetworkProxy isn't configured or as a fallback
@@ -1235,37 +505,15 @@ export class PortProxy {
1235
505
  initialChunk?: Buffer,
1236
506
  overridePort?: number
1237
507
  ): void {
1238
- // Enhanced logging for initial connection troubleshooting
1239
- if (serverName) {
1240
- console.log(`[${connectionId}] Setting up direct connection for domain: ${serverName}`);
1241
- } else {
1242
- console.log(`[${connectionId}] Setting up direct connection without SNI`);
1243
- }
1244
-
1245
- // Log domain config details to help diagnose routing issues
1246
- if (domainConfig) {
1247
- console.log(`[${connectionId}] Using domain config: ${domainConfig.domains.join(', ')}`);
1248
- } else {
1249
- console.log(`[${connectionId}] No specific domain config found, using default settings`);
1250
- }
1251
-
1252
- // Ensure we maximize connection chances by setting appropriate timeouts
1253
- socket.setTimeout(30000); // 30 second initial connect timeout
1254
-
1255
508
  // Existing connection setup logic
1256
509
  const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
1257
510
  const connectionOptions: plugins.net.NetConnectOpts = {
1258
511
  host: targetHost,
1259
512
  port: overridePort !== undefined ? overridePort : this.settings.toPort,
1260
- // Add connection timeout to ensure we don't hang indefinitely
1261
- timeout: 15000 // 15 second connection timeout
1262
513
  };
1263
514
  if (this.settings.preserveSourceIP) {
1264
515
  connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
1265
516
  }
1266
-
1267
- console.log(`[${connectionId}] Connecting to backend: ${targetHost}:${connectionOptions.port}`);
1268
-
1269
517
 
1270
518
  // Pause the incoming socket to prevent buffer overflows
1271
519
  socket.pause();
@@ -1306,22 +554,11 @@ export class PortProxy {
1306
554
  // Add the temp handler to capture all incoming data during connection setup
1307
555
  socket.on('data', tempDataHandler);
1308
556
 
1309
- // Add initial chunk to pending data if present - this is critical for SNI forwarding
557
+ // Add initial chunk to pending data if present
1310
558
  if (initialChunk) {
1311
- // Make explicit copy of the buffer to ensure it doesn't get modified
1312
- const initialDataCopy = Buffer.from(initialChunk);
1313
- record.bytesReceived += initialDataCopy.length;
1314
- record.pendingData.push(initialDataCopy);
1315
- record.pendingDataSize = initialDataCopy.length;
1316
-
1317
- // Log TLS handshake for debug purposes
1318
- if (isTlsHandshake(initialChunk)) {
1319
- record.isTLS = true;
1320
- console.log(`[${connectionId}] Buffered TLS handshake data: ${initialDataCopy.length} bytes, SNI: ${serverName || 'none'}`);
1321
- }
1322
- } else if (record.isTLS) {
1323
- // This shouldn't happen, but log a warning if we have a TLS connection with no initial data
1324
- console.log(`[${connectionId}] WARNING: TLS connection without initial handshake data`);
559
+ record.bytesReceived += initialChunk.length;
560
+ record.pendingData.push(Buffer.from(initialChunk));
561
+ record.pendingDataSize = initialChunk.length;
1325
562
  }
1326
563
 
1327
564
  // Create the target socket but don't set up piping immediately
@@ -1331,11 +568,11 @@ export class PortProxy {
1331
568
 
1332
569
  // Apply socket optimizations
1333
570
  targetSocket.setNoDelay(this.settings.noDelay);
1334
-
571
+
1335
572
  // Apply keep-alive settings to the outgoing connection as well
1336
573
  if (this.settings.keepAlive) {
1337
574
  targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
1338
-
575
+
1339
576
  // Apply enhanced TCP keep-alive options if enabled
1340
577
  if (this.settings.enableKeepAliveProbes) {
1341
578
  try {
@@ -1348,15 +585,13 @@ export class PortProxy {
1348
585
  } catch (err) {
1349
586
  // Ignore errors - these are optional enhancements
1350
587
  if (this.settings.enableDetailedLogging) {
1351
- console.log(
1352
- `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
1353
- );
588
+ console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
1354
589
  }
1355
590
  }
1356
591
  }
1357
592
  }
1358
593
 
1359
- // Setup specific error handler for connection phase with enhanced retries
594
+ // Setup specific error handler for connection phase
1360
595
  targetSocket.once('error', (err) => {
1361
596
  // This handler runs only once during the initial connection phase
1362
597
  const code = (err as any).code;
@@ -1367,7 +602,6 @@ export class PortProxy {
1367
602
  // Resume the incoming socket to prevent it from hanging
1368
603
  socket.resume();
1369
604
 
1370
- // Add detailed logging for connection problems
1371
605
  if (code === 'ECONNREFUSED') {
1372
606
  console.log(
1373
607
  `[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
@@ -1383,28 +617,6 @@ export class PortProxy {
1383
617
  } else if (code === 'EHOSTUNREACH') {
1384
618
  console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
1385
619
  }
1386
-
1387
- // Log additional diagnostics
1388
- console.log(`[${connectionId}] Connection details - SNI: ${serverName || 'none'}, HasChunk: ${!!initialChunk}, ChunkSize: ${initialChunk ? initialChunk.length : 0}`);
1389
-
1390
- // For TLS connections, provide even more detailed diagnostics
1391
- if (record.isTLS) {
1392
- console.log(`[${connectionId}] TLS connection failure details - TLS detected: ${record.isTLS}, Server: ${targetHost}:${connectionOptions.port}, Domain config: ${domainConfig ? 'Present' : 'Missing'}`);
1393
- }
1394
-
1395
- // For connection refusal or timeouts, try a more aggressive error response
1396
- // This helps browsers quickly realize there's an issue rather than waiting
1397
- if (code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'EHOSTUNREACH') {
1398
- try {
1399
- // Send a RST packet rather than a graceful close
1400
- // This signals to browsers to try a new connection immediately
1401
- socket.destroy(new Error(`Backend connection failed: ${code}`));
1402
- console.log(`[${connectionId}] Forced connection termination to trigger immediate browser retry`);
1403
- return; // Skip normal cleanup
1404
- } catch (destroyErr) {
1405
- console.log(`[${connectionId}] Error during forced connection termination: ${destroyErr}`);
1406
- }
1407
- }
1408
620
 
1409
621
  // Clear any existing error handler after connection phase
1410
622
  targetSocket.removeAllListeners('error');
@@ -1430,21 +642,19 @@ export class PortProxy {
1430
642
  // For keep-alive connections, just log a warning instead of closing
1431
643
  if (record.hasKeepAlive) {
1432
644
  console.log(
1433
- `[${connectionId}] Timeout event on incoming keep-alive connection from ${
1434
- record.remoteIP
1435
- } after ${plugins.prettyMs(
645
+ `[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
1436
646
  this.settings.socketTimeout || 3600000
1437
647
  )}. Connection preserved.`
1438
648
  );
1439
649
  // Don't close the connection - just log
1440
650
  return;
1441
651
  }
1442
-
652
+
1443
653
  // For non-keep-alive connections, proceed with normal cleanup
1444
654
  console.log(
1445
- `[${connectionId}] Timeout on incoming side from ${
1446
- record.remoteIP
1447
- } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
655
+ `[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(
656
+ this.settings.socketTimeout || 3600000
657
+ )}`
1448
658
  );
1449
659
  if (record.incomingTerminationReason === null) {
1450
660
  record.incomingTerminationReason = 'timeout';
@@ -1457,21 +667,19 @@ export class PortProxy {
1457
667
  // For keep-alive connections, just log a warning instead of closing
1458
668
  if (record.hasKeepAlive) {
1459
669
  console.log(
1460
- `[${connectionId}] Timeout event on outgoing keep-alive connection from ${
1461
- record.remoteIP
1462
- } after ${plugins.prettyMs(
670
+ `[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
1463
671
  this.settings.socketTimeout || 3600000
1464
672
  )}. Connection preserved.`
1465
673
  );
1466
674
  // Don't close the connection - just log
1467
675
  return;
1468
676
  }
1469
-
677
+
1470
678
  // For non-keep-alive connections, proceed with normal cleanup
1471
679
  console.log(
1472
- `[${connectionId}] Timeout on outgoing side from ${
1473
- record.remoteIP
1474
- } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
680
+ `[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(
681
+ this.settings.socketTimeout || 3600000
682
+ )}`
1475
683
  );
1476
684
  if (record.outgoingTerminationReason === null) {
1477
685
  record.outgoingTerminationReason = 'timeout';
@@ -1485,11 +693,9 @@ export class PortProxy {
1485
693
  // Disable timeouts completely for immortal connections
1486
694
  socket.setTimeout(0);
1487
695
  targetSocket.setTimeout(0);
1488
-
696
+
1489
697
  if (this.settings.enableDetailedLogging) {
1490
- console.log(
1491
- `[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`
1492
- );
698
+ console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
1493
699
  }
1494
700
  } else {
1495
701
  // Set normal timeouts for other connections
@@ -1517,105 +723,14 @@ export class PortProxy {
1517
723
  // Flush all pending data to target
1518
724
  if (record.pendingData.length > 0) {
1519
725
  const combinedData = Buffer.concat(record.pendingData);
1520
-
1521
- // Add critical debugging for SNI forwarding issues
1522
- if (record.isTLS && this.settings.enableTlsDebugLogging) {
1523
- console.log(`[${connectionId}] Forwarding TLS handshake data: ${combinedData.length} bytes, SNI: ${serverName || 'none'}`);
1524
-
1525
- // Additional check to verify we're forwarding the ClientHello properly
1526
- if (combinedData[0] === 22) { // TLS handshake
1527
- console.log(`[${connectionId}] Initial data is a TLS handshake record`);
1528
- }
1529
- }
1530
-
1531
- // Write the combined data to the target
1532
726
  targetSocket.write(combinedData, (err) => {
1533
727
  if (err) {
1534
- console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
728
+ console.log(
729
+ `[${connectionId}] Error writing pending data to target: ${err.message}`
730
+ );
1535
731
  return this.initiateCleanupOnce(record, 'write_error');
1536
732
  }
1537
-
1538
- if (record.isTLS) {
1539
- // Log successful forwarding of initial TLS data
1540
- console.log(`[${connectionId}] Successfully forwarded initial TLS data to backend`);
1541
- }
1542
733
 
1543
- // Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
1544
- if (serverName && record.isTLS) {
1545
- // This listener handles TLS renegotiation detection
1546
- socket.on('data', (renegChunk) => {
1547
- if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
1548
- // Always update activity timestamp for any handshake packet
1549
- this.updateActivity(record);
1550
-
1551
- try {
1552
- // Extract all TLS information including session resumption data
1553
- const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
1554
- let newSNI = sniInfo?.serverName;
1555
-
1556
- // Handle session resumption - if we recognize the session ID, we know what domain it belongs to
1557
- if (sniInfo?.isResumption && sniInfo.resumedDomain) {
1558
- console.log(`[${connectionId}] Rehandshake with session resumption for domain: ${sniInfo.resumedDomain}`);
1559
- newSNI = sniInfo.resumedDomain;
1560
- }
1561
-
1562
- // IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through
1563
- if (newSNI === undefined) {
1564
- console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
1565
- return;
1566
- }
1567
-
1568
- // Check if the SNI has changed
1569
- if (newSNI !== serverName) {
1570
- console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
1571
-
1572
- // Allow if the new SNI matches existing domain config or find a new matching config
1573
- let allowed = false;
1574
-
1575
- if (record.domainConfig) {
1576
- allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
1577
- }
1578
-
1579
- if (!allowed) {
1580
- const newDomainConfig = this.settings.domainConfigs.find((config) =>
1581
- config.domains.some((d) => plugins.minimatch(newSNI, d))
1582
- );
1583
-
1584
- if (newDomainConfig) {
1585
- const effectiveAllowedIPs = [
1586
- ...newDomainConfig.allowedIPs,
1587
- ...(this.settings.defaultAllowedIPs || []),
1588
- ];
1589
- const effectiveBlockedIPs = [
1590
- ...(newDomainConfig.blockedIPs || []),
1591
- ...(this.settings.defaultBlockedIPs || []),
1592
- ];
1593
-
1594
- allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
1595
-
1596
- if (allowed) {
1597
- record.domainConfig = newDomainConfig;
1598
- }
1599
- }
1600
- }
1601
-
1602
- if (allowed) {
1603
- console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
1604
- record.lockedDomain = newSNI;
1605
- } else {
1606
- console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
1607
- this.initiateCleanupOnce(record, 'sni_mismatch');
1608
- }
1609
- } else {
1610
- console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
1611
- }
1612
- } catch (err) {
1613
- console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
1614
- }
1615
- }
1616
- });
1617
- }
1618
-
1619
734
  // Now set up piping for future data and resume the socket
1620
735
  socket.pipe(targetSocket);
1621
736
  targetSocket.pipe(socket);
@@ -1631,9 +746,7 @@ export class PortProxy {
1631
746
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1632
747
  : ''
1633
748
  }` +
1634
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1635
- record.hasKeepAlive ? 'Yes' : 'No'
1636
- }`
749
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
1637
750
  );
1638
751
  } else {
1639
752
  console.log(
@@ -1645,118 +758,11 @@ export class PortProxy {
1645
758
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1646
759
  : ''
1647
760
  }`
1648
- );
1649
- }
1650
- });
1651
- } else {
1652
- // Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
1653
- if (serverName && record.isTLS) {
1654
- // This listener handles TLS renegotiation detection
1655
- socket.on('data', (renegChunk) => {
1656
- if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
1657
- // Always update activity timestamp for any handshake packet
1658
- this.updateActivity(record);
1659
-
1660
- try {
1661
- // Extract all TLS information including session resumption data
1662
- const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
1663
- let newSNI = sniInfo?.serverName;
1664
-
1665
- // Handle session resumption - if we recognize the session ID, we know what domain it belongs to
1666
- if (sniInfo?.isResumption && sniInfo.resumedDomain) {
1667
- console.log(`[${connectionId}] Rehandshake with session resumption for domain: ${sniInfo.resumedDomain}`);
1668
- newSNI = sniInfo.resumedDomain;
1669
- }
1670
-
1671
- // IMPORTANT: If we can't extract an SNI from renegotiation, we MUST allow it through
1672
- if (newSNI === undefined) {
1673
- console.log(`[${connectionId}] Rehandshake detected without SNI, allowing it through.`);
1674
- return;
1675
- }
1676
-
1677
- // Check if the SNI has changed
1678
- if (newSNI !== serverName) {
1679
- console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
1680
-
1681
- // Allow if the new SNI matches existing domain config or find a new matching config
1682
- let allowed = false;
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
1686
- if (record.domainConfig) {
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
- }
1692
- }
1693
-
1694
- // If not allowed by existing config, try to find an alternative domain config
1695
- if (!allowed) {
1696
- // First try exact match
1697
- let newDomainConfig = this.settings.domainConfigs.find((config) =>
1698
- config.domains.some((d) => plugins.minimatch(newSNI, d))
1699
- );
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
-
1724
- if (newDomainConfig) {
1725
- const effectiveAllowedIPs = [
1726
- ...newDomainConfig.allowedIPs,
1727
- ...(this.settings.defaultAllowedIPs || []),
1728
- ];
1729
- const effectiveBlockedIPs = [
1730
- ...(newDomainConfig.blockedIPs || []),
1731
- ...(this.settings.defaultBlockedIPs || []),
1732
- ];
1733
-
1734
- allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
1735
-
1736
- if (allowed) {
1737
- record.domainConfig = newDomainConfig;
1738
- }
1739
- }
1740
- }
1741
-
1742
- if (allowed) {
1743
- console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
1744
- record.lockedDomain = newSNI;
1745
- } else {
1746
- console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
1747
- this.initiateCleanupOnce(record, 'sni_mismatch');
1748
- }
1749
- } else {
1750
- console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
1751
- }
1752
- } catch (err) {
1753
- console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
1754
- }
1755
- }
1756
- });
1757
- }
1758
-
1759
- // Now set up piping
761
+ );
762
+ }
763
+ });
764
+ } else {
765
+ // No pending data, so just set up piping
1760
766
  socket.pipe(targetSocket);
1761
767
  targetSocket.pipe(socket);
1762
768
  socket.resume(); // Resume the socket after piping is established
@@ -1771,9 +777,7 @@ export class PortProxy {
1771
777
  ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1772
778
  : ''
1773
779
  }` +
1774
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1775
- record.hasKeepAlive ? 'Yes' : 'No'
1776
- }`
780
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
1777
781
  );
1778
782
  } else {
1779
783
  console.log(
@@ -1793,98 +797,82 @@ export class PortProxy {
1793
797
  record.pendingData = [];
1794
798
  record.pendingDataSize = 0;
1795
799
 
1796
- // Renegotiation detection is now handled before piping is established
1797
- // This ensures the data listener receives all packets properly
800
+ // Add the renegotiation listener for SNI validation
801
+ if (serverName) {
802
+ socket.on('data', (renegChunk: Buffer) => {
803
+ if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
804
+ try {
805
+ // Try to extract SNI from potential renegotiation
806
+ const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
807
+ if (newSNI && newSNI !== record.lockedDomain) {
808
+ console.log(
809
+ `[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${record.lockedDomain}. Terminating connection.`
810
+ );
811
+ this.initiateCleanupOnce(record, 'sni_mismatch');
812
+ } else if (newSNI && this.settings.enableDetailedLogging) {
813
+ console.log(
814
+ `[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
815
+ );
816
+ }
817
+ } catch (err) {
818
+ console.log(
819
+ `[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
820
+ );
821
+ }
822
+ }
823
+ });
824
+ }
1798
825
 
1799
826
  // Set connection timeout with simpler logic
1800
827
  if (record.cleanupTimer) {
1801
828
  clearTimeout(record.cleanupTimer);
1802
829
  }
1803
-
830
+
1804
831
  // For immortal keep-alive connections, skip setting a timeout completely
1805
832
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
1806
833
  if (this.settings.enableDetailedLogging) {
1807
- console.log(
1808
- `[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
1809
- );
834
+ console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
1810
835
  }
1811
836
  // No cleanup timer for immortal connections
1812
- }
1813
- // For TLS keep-alive connections, use a more generous timeout now that
1814
- // we've fixed the renegotiation handling issue that was causing certificate problems
1815
- else if (record.hasKeepAlive && record.isTLS) {
1816
- // Use a longer timeout for TLS connections now that renegotiation handling is fixed
1817
- // This reduces unnecessary reconnections while still ensuring certificate freshness
1818
- const tlsKeepAliveTimeout = 4 * 60 * 60 * 1000; // 4 hours for TLS keep-alive - increased from 30 minutes
1819
- const safeTimeout = ensureSafeTimeout(tlsKeepAliveTimeout);
1820
-
1821
- record.cleanupTimer = setTimeout(() => {
1822
- console.log(
1823
- `[${connectionId}] TLS keep-alive connection from ${
1824
- record.remoteIP
1825
- } exceeded max lifetime (${plugins.prettyMs(
1826
- tlsKeepAliveTimeout
1827
- )}), forcing cleanup to refresh certificate context.`
1828
- );
1829
- this.initiateCleanupOnce(record, 'tls_certificate_refresh');
1830
- }, safeTimeout);
1831
-
1832
- // Make sure timeout doesn't keep the process alive
1833
- if (record.cleanupTimer.unref) {
1834
- record.cleanupTimer.unref();
1835
- }
1836
-
1837
- if (this.settings.enableDetailedLogging) {
1838
- console.log(
1839
- `[${connectionId}] TLS keep-alive connection with aggressive certificate refresh protection, lifetime: ${plugins.prettyMs(
1840
- tlsKeepAliveTimeout
1841
- )}`
1842
- );
1843
- }
1844
- }
837
+ }
1845
838
  // For extended keep-alive connections, use extended timeout
1846
839
  else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
1847
840
  const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
1848
841
  const safeTimeout = ensureSafeTimeout(extendedTimeout);
1849
-
842
+
1850
843
  record.cleanupTimer = setTimeout(() => {
1851
844
  console.log(
1852
- `[${connectionId}] Keep-alive connection from ${
1853
- record.remoteIP
1854
- } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
845
+ `[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs(
846
+ extendedTimeout
847
+ )}), forcing cleanup.`
1855
848
  );
1856
849
  this.initiateCleanupOnce(record, 'extended_lifetime');
1857
850
  }, safeTimeout);
1858
-
851
+
1859
852
  // Make sure timeout doesn't keep the process alive
1860
853
  if (record.cleanupTimer.unref) {
1861
854
  record.cleanupTimer.unref();
1862
855
  }
1863
-
856
+
1864
857
  if (this.settings.enableDetailedLogging) {
1865
- console.log(
1866
- `[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
1867
- extendedTimeout
1868
- )}`
1869
- );
858
+ console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
1870
859
  }
1871
860
  }
1872
861
  // For standard connections, use normal timeout
1873
862
  else {
1874
863
  // Use domain-specific timeout if available, otherwise use default
1875
- const connectionTimeout =
1876
- record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
864
+ const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
1877
865
  const safeTimeout = ensureSafeTimeout(connectionTimeout);
1878
-
866
+
1879
867
  record.cleanupTimer = setTimeout(() => {
1880
868
  console.log(
1881
- `[${connectionId}] Connection from ${
1882
- record.remoteIP
1883
- } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
869
+ `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs(
870
+ connectionTimeout
871
+ )}), forcing cleanup.`
1884
872
  );
1885
873
  this.initiateCleanupOnce(record, 'connection_timeout');
1886
874
  }, safeTimeout);
1887
-
875
+
1888
876
  // Make sure timeout doesn't keep the process alive
1889
877
  if (record.cleanupTimer.unref) {
1890
878
  record.cleanupTimer.unref();
@@ -1962,219 +950,6 @@ export class PortProxy {
1962
950
  this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
1963
951
  }
1964
952
 
1965
- /**
1966
- * Update connection activity timestamp with enhanced sleep detection
1967
- * Improved for chained proxy scenarios and more aggressive handling of stale connections
1968
- */
1969
- private updateActivity(record: IConnectionRecord): void {
1970
- // Get the current time
1971
- const now = Date.now();
1972
-
1973
- // Check if there was a large time gap that suggests system sleep
1974
- if (record.lastActivity > 0) {
1975
- const timeDiff = now - record.lastActivity;
1976
-
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) {
1995
- console.log(
1996
- `[${record.id}] Detected possible short inactivity for ${plugins.prettyMs(timeDiff)}. ` +
1997
- `Monitoring for TLS connection health.`
1998
- );
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) {
2023
- console.log(
2024
- `[${record.id}] TLS connection in chained proxy inactive for ${plugins.prettyMs(timeDiff)}. ` +
2025
- `Closing to prevent certificate inconsistencies across chain.`
2026
- );
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) {
2047
- console.log(
2048
- `[${refreshCheckId}] No activity detected after TLS refresh (${plugins.prettyMs(verificationTimeDiff)}). ` +
2049
- `Closing connection to ensure proper browser reconnection.`
2050
- );
2051
- this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
2052
- }
2053
- }
2054
- }, verificationTimeout);
2055
-
2056
- // Make sure timeout doesn't keep the process alive
2057
- if (refreshCheck.unref) {
2058
- refreshCheck.unref();
2059
- }
2060
- }
2061
-
2062
- // Update sleep detection markers
2063
- record.possibleSystemSleep = true;
2064
- record.lastSleepDetection = now;
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
- }
2078
- }
2079
-
2080
- // Update the activity timestamp
2081
- record.lastActivity = now;
2082
-
2083
- // Clear any inactivity warning
2084
- if (record.inactivityWarningIssued) {
2085
- record.inactivityWarningIssued = false;
2086
- }
2087
- }
2088
-
2089
- /**
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
2093
- */
2094
- private performDeepTlsRefresh(record: IConnectionRecord): void {
2095
- // Skip if we're using a NetworkProxy as it handles its own TLS state
2096
- if (record.usingNetworkProxy) {
2097
- return;
2098
- }
2099
-
2100
- try {
2101
- // For outgoing connections that might need to be refreshed
2102
- if (record.outgoing && !record.outgoing.destroyed) {
2103
- // Check how long this connection has been established
2104
- const connectionAge = Date.now() - record.incomingStartTime;
2105
- const hourInMs = 60 * 60 * 1000;
2106
-
2107
- // For very long-lived connections, just close them
2108
- if (connectionAge > 4 * hourInMs) { // Reduced from 8 hours to 4 hours for chained proxies
2109
- console.log(
2110
- `[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
2111
- `Closing to ensure proper certificate handling across proxy chain.`
2112
- );
2113
- return this.initiateCleanupOnce(record, 'certificate_age_refresh');
2114
- }
2115
-
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
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);
2157
-
2158
- if (this.settings.enableDetailedLogging) {
2159
- console.log(`[${record.id}] Initiated deep TLS refresh sequence`);
2160
- }
2161
- }
2162
- } catch (err) {
2163
- console.log(`[${record.id}] Error starting TLS state refresh: ${err}`);
2164
-
2165
- // If we hit an error, it's likely the connection is already broken
2166
- // Force cleanup to ensure browser reconnects cleanly
2167
- return this.initiateCleanupOnce(record, 'tls_refresh_error');
2168
- }
2169
- }
2170
-
2171
- /**
2172
- * Legacy refresh method for backward compatibility
2173
- */
2174
- private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
2175
- return this.performDeepTlsRefresh(record);
2176
- }
2177
-
2178
953
  /**
2179
954
  * Cleans up a connection record.
2180
955
  * Destroys both incoming and outgoing sockets, clears timers, and removes the record.
@@ -2272,9 +1047,7 @@ export class PortProxy {
2272
1047
  ` Duration: ${plugins.prettyMs(
2273
1048
  duration
2274
1049
  )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
2275
- `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
2276
- record.hasKeepAlive ? 'Yes' : 'No'
2277
- }` +
1050
+ `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
2278
1051
  `${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
2279
1052
  );
2280
1053
  } else {
@@ -2285,6 +1058,18 @@ export class PortProxy {
2285
1058
  }
2286
1059
  }
2287
1060
 
1061
+ /**
1062
+ * Update connection activity timestamp
1063
+ */
1064
+ private updateActivity(record: IConnectionRecord): void {
1065
+ record.lastActivity = Date.now();
1066
+
1067
+ // Clear any inactivity warning
1068
+ if (record.inactivityWarningIssued) {
1069
+ record.inactivityWarningIssued = false;
1070
+ }
1071
+ }
1072
+
2288
1073
  /**
2289
1074
  * Get target IP with round-robin support
2290
1075
  */
@@ -2297,7 +1082,7 @@ export class PortProxy {
2297
1082
  }
2298
1083
  return this.settings.targetIP!;
2299
1084
  }
2300
-
1085
+
2301
1086
  /**
2302
1087
  * Initiates cleanup once for a connection
2303
1088
  */
@@ -2305,15 +1090,12 @@ export class PortProxy {
2305
1090
  if (this.settings.enableDetailedLogging) {
2306
1091
  console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
2307
1092
  }
2308
-
2309
- if (
2310
- record.incomingTerminationReason === null ||
2311
- record.incomingTerminationReason === undefined
2312
- ) {
1093
+
1094
+ if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) {
2313
1095
  record.incomingTerminationReason = reason;
2314
1096
  this.incrementTerminationStat('incoming', reason);
2315
1097
  }
2316
-
1098
+
2317
1099
  this.cleanupConnection(record, reason);
2318
1100
  }
2319
1101
 
@@ -2437,7 +1219,7 @@ export class PortProxy {
2437
1219
 
2438
1220
  // Apply socket optimizations
2439
1221
  socket.setNoDelay(this.settings.noDelay);
2440
-
1222
+
2441
1223
  // Create a unique connection ID and record
2442
1224
  const connectionId = generateConnectionId();
2443
1225
  const connectionRecord: IConnectionRecord = {
@@ -2461,19 +1243,16 @@ export class PortProxy {
2461
1243
  hasKeepAlive: false, // Will set to true if keep-alive is applied
2462
1244
  incomingTerminationReason: null,
2463
1245
  outgoingTerminationReason: null,
2464
-
1246
+
2465
1247
  // Initialize NetworkProxy tracking fields
2466
- usingNetworkProxy: false,
2467
-
2468
- // Initialize sleep detection fields
2469
- possibleSystemSleep: false,
1248
+ usingNetworkProxy: false
2470
1249
  };
2471
-
1250
+
2472
1251
  // Apply keep-alive settings if enabled
2473
1252
  if (this.settings.keepAlive) {
2474
1253
  socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
2475
1254
  connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
2476
-
1255
+
2477
1256
  // Apply enhanced TCP keep-alive options if enabled
2478
1257
  if (this.settings.enableKeepAliveProbes) {
2479
1258
  try {
@@ -2487,9 +1266,7 @@ export class PortProxy {
2487
1266
  } catch (err) {
2488
1267
  // Ignore errors - these are optional enhancements
2489
1268
  if (this.settings.enableDetailedLogging) {
2490
- console.log(
2491
- `[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
2492
- );
1269
+ console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
2493
1270
  }
2494
1271
  }
2495
1272
  }
@@ -2502,8 +1279,8 @@ export class PortProxy {
2502
1279
  if (this.settings.enableDetailedLogging) {
2503
1280
  console.log(
2504
1281
  `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
2505
- `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
2506
- `Active connections: ${this.connectionRecords.size}`
1282
+ `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
1283
+ `Active connections: ${this.connectionRecords.size}`
2507
1284
  );
2508
1285
  } else {
2509
1286
  console.log(
@@ -2607,95 +1384,16 @@ export class PortProxy {
2607
1384
  }
2608
1385
 
2609
1386
  // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
2610
- let domainConfig = forcedDomain
1387
+ const domainConfig = forcedDomain
2611
1388
  ? forcedDomain
2612
1389
  : serverName
2613
1390
  ? this.settings.domainConfigs.find((config) =>
2614
1391
  config.domains.some((d) => plugins.minimatch(serverName, d))
2615
1392
  )
2616
1393
  : undefined;
2617
-
2618
- // Enhanced logging to diagnose domain config selection issues
2619
- if (serverName && !domainConfig) {
2620
- console.log(`[${connectionId}] WARNING: No domain config found for SNI: ${serverName}`);
2621
- console.log(`[${connectionId}] Available domains:`,
2622
- this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
2623
- } else if (serverName && domainConfig) {
2624
- console.log(`[${connectionId}] Found domain config for SNI: ${serverName} -> ${domainConfig.domains.join(',')}`);
2625
- }
2626
1394
 
2627
- // For session resumption, ensure we use the domain config matching the resumed domain
2628
- // The resumed domain will be in serverName if this is a session resumption
2629
- if (serverName && connectionRecord.lockedDomain === serverName && serverName !== '') {
2630
- // Override domain config lookup for session resumption - crucial for certificate selection
2631
-
2632
- // First try an exact match
2633
- let resumedDomainConfig = this.settings.domainConfigs.find((config) =>
2634
- config.domains.some((d) => plugins.minimatch(serverName, d))
2635
- );
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
-
2661
- if (resumedDomainConfig) {
2662
- domainConfig = resumedDomainConfig;
2663
- console.log(`[${connectionId}] Found domain config for resumed session: ${serverName} -> ${resumedDomainConfig.domains.join(',')}`);
2664
- } else {
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
- }
2688
- }
2689
- }
2690
-
2691
1395
  // Save domain config in connection record
2692
1396
  connectionRecord.domainConfig = domainConfig;
2693
-
2694
- // Always set the lockedDomain, even for non-SNI connections
2695
- if (serverName) {
2696
- connectionRecord.lockedDomain = serverName;
2697
- console.log(`[${connectionId}] Locked connection to domain: ${serverName}`);
2698
- }
2699
1397
 
2700
1398
  // IP validation is skipped if allowedIPs is empty
2701
1399
  if (domainConfig) {
@@ -2720,12 +1418,12 @@ export class PortProxy {
2720
1418
  )}`
2721
1419
  );
2722
1420
  }
2723
-
1421
+
2724
1422
  // Check if we should forward this to a NetworkProxy
2725
1423
  if (
2726
- isTlsHandshakeDetected &&
2727
- domainConfig.useNetworkProxy === true &&
2728
- initialChunk &&
1424
+ isTlsHandshakeDetected &&
1425
+ domainConfig.useNetworkProxy === true &&
1426
+ initialChunk &&
2729
1427
  this.networkProxies.length > 0
2730
1428
  ) {
2731
1429
  return this.forwardToNetworkProxy(
@@ -2852,49 +1550,19 @@ export class PortProxy {
2852
1550
 
2853
1551
  initialDataReceived = true;
2854
1552
 
2855
- // Try to extract SNI - with enhanced logging for troubleshooting
1553
+ // Try to extract SNI
2856
1554
  let serverName = '';
2857
-
2858
- // Record the chunk size for diagnostic purposes
2859
- console.log(`[${connectionId}] Received initial data: ${chunk.length} bytes`);
2860
1555
 
2861
1556
  if (isTlsHandshake(chunk)) {
2862
1557
  connectionRecord.isTLS = true;
2863
1558
 
2864
- console.log(`[${connectionId}] Detected TLS handshake`);
2865
-
2866
1559
  if (this.settings.enableTlsDebugLogging) {
2867
1560
  console.log(
2868
1561
  `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
2869
1562
  );
2870
1563
  }
2871
1564
 
2872
- // Extract all TLS information including session resumption
2873
- const sniInfo = extractSNIInfo(chunk, this.settings.enableTlsDebugLogging);
2874
-
2875
- if (sniInfo?.isResumption && sniInfo.resumedDomain) {
2876
- // This is a session resumption with a known domain
2877
- serverName = sniInfo.resumedDomain;
2878
- console.log(`[${connectionId}] TLS Session resumption detected for domain: ${serverName}`);
2879
-
2880
- // When resuming a session, explicitly set the domain in the record to ensure proper routing
2881
- // This is CRITICAL for ensuring we select the correct backend/certificate
2882
- connectionRecord.lockedDomain = serverName;
2883
-
2884
- // Force detailed logging for resumed sessions to help with troubleshooting
2885
- console.log(`[${connectionId}] Resuming TLS session for domain ${serverName} - will use original certificate`);
2886
- } else {
2887
- // Normal SNI extraction
2888
- serverName = sniInfo?.serverName || '';
2889
-
2890
- if (serverName) {
2891
- console.log(`[${connectionId}] Extracted SNI domain: ${serverName}`);
2892
- } else {
2893
- console.log(`[${connectionId}] No SNI found in TLS handshake`);
2894
- }
2895
- }
2896
- } else {
2897
- console.log(`[${connectionId}] Non-TLS connection detected`);
1565
+ serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
2898
1566
  }
2899
1567
 
2900
1568
  // Lock the connection to the negotiated SNI.
@@ -2993,11 +1661,11 @@ export class PortProxy {
2993
1661
  } else {
2994
1662
  nonTlsConnections++;
2995
1663
  }
2996
-
1664
+
2997
1665
  if (record.hasKeepAlive) {
2998
1666
  keepAliveConnections++;
2999
1667
  }
3000
-
1668
+
3001
1669
  if (record.usingNetworkProxy) {
3002
1670
  networkProxyConnections++;
3003
1671
  }
@@ -3038,80 +1706,35 @@ export class PortProxy {
3038
1706
  }
3039
1707
 
3040
1708
  // Skip inactivity check if disabled or for immortal keep-alive connections
3041
- if (
3042
- !this.settings.disableInactivityCheck &&
3043
- !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
3044
- ) {
1709
+ if (!this.settings.disableInactivityCheck &&
1710
+ !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
1711
+
3045
1712
  const inactivityTime = now - record.lastActivity;
3046
-
3047
- // Special handling for TLS keep-alive connections
3048
- if (
3049
- record.hasKeepAlive &&
3050
- record.isTLS &&
3051
- inactivityTime > this.settings.inactivityTimeout! / 2
3052
- ) {
3053
- // For TLS keep-alive connections that are getting stale, try to refresh before closing
3054
- if (!record.inactivityWarningIssued) {
3055
- console.log(
3056
- `[${id}] TLS keep-alive connection from ${
3057
- record.remoteIP
3058
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
3059
- `Attempting to preserve connection.`
3060
- );
3061
-
3062
- // Set warning flag but give a much longer grace period for TLS connections
3063
- record.inactivityWarningIssued = true;
3064
-
3065
- // For TLS connections, extend the last activity time considerably
3066
- // This gives browsers more time to re-establish the connection properly
3067
- record.lastActivity = now - this.settings.inactivityTimeout! / 3;
3068
-
3069
- // Try to stimulate the connection with a probe packet
3070
- if (record.outgoing && !record.outgoing.destroyed) {
3071
- try {
3072
- // For TLS connections, send a proper TLS heartbeat-like packet
3073
- // This is just a small empty buffer that won't affect the TLS session
3074
- record.outgoing.write(Buffer.alloc(0));
3075
-
3076
- if (this.settings.enableDetailedLogging) {
3077
- console.log(`[${id}] Sent TLS keep-alive probe packet`);
3078
- }
3079
- } catch (err) {
3080
- console.log(`[${id}] Error sending TLS probe packet: ${err}`);
3081
- }
3082
- }
3083
-
3084
- // Don't proceed to the normal inactivity check logic
3085
- continue;
3086
- }
3087
- }
3088
-
1713
+
3089
1714
  // Use extended timeout for extended-treatment keep-alive connections
3090
1715
  let effectiveTimeout = this.settings.inactivityTimeout!;
3091
1716
  if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
3092
1717
  const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
3093
1718
  effectiveTimeout = effectiveTimeout * multiplier;
3094
1719
  }
3095
-
1720
+
3096
1721
  if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
3097
1722
  // For keep-alive connections, issue a warning first
3098
1723
  if (record.hasKeepAlive && !record.inactivityWarningIssued) {
3099
1724
  console.log(
3100
- `[${id}] Warning: Keep-alive connection from ${
3101
- record.remoteIP
3102
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
3103
- `Will close in 10 minutes if no activity.`
1725
+ `[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
1726
+ `Will close in 10 minutes if no activity.`
3104
1727
  );
3105
-
1728
+
3106
1729
  // Set warning flag and add grace period
3107
1730
  record.inactivityWarningIssued = true;
3108
1731
  record.lastActivity = now - (effectiveTimeout - 600000);
3109
-
1732
+
3110
1733
  // Try to stimulate activity with a probe packet
3111
1734
  if (record.outgoing && !record.outgoing.destroyed) {
3112
1735
  try {
3113
1736
  record.outgoing.write(Buffer.alloc(0));
3114
-
1737
+
3115
1738
  if (this.settings.enableDetailedLogging) {
3116
1739
  console.log(`[${id}] Sent probe packet to test keep-alive connection`);
3117
1740
  }
@@ -3120,48 +1743,18 @@ export class PortProxy {
3120
1743
  }
3121
1744
  }
3122
1745
  } else {
3123
- // MODIFIED: For TLS connections, be more lenient before closing
3124
- // For TLS browser connections, we need to handle certificate context properly
3125
- if (record.isTLS && record.hasKeepAlive) {
3126
- // For very long inactivity, it's better to close the connection
3127
- // so the browser establishes a new one with a fresh certificate context
3128
- if (inactivityTime > 6 * 60 * 60 * 1000) {
3129
- // 6 hours
3130
- console.log(
3131
- `[${id}] TLS keep-alive connection from ${
3132
- record.remoteIP
3133
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
3134
- `Closing to ensure proper certificate handling on browser reconnect.`
3135
- );
3136
- this.cleanupConnection(record, 'tls_certificate_refresh');
3137
- } else {
3138
- // For shorter inactivity periods, add grace period
3139
- console.log(
3140
- `[${id}] TLS keep-alive connection from ${
3141
- record.remoteIP
3142
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
3143
- `Adding extra grace period.`
3144
- );
3145
-
3146
- // Give additional time for browsers to reconnect properly
3147
- record.lastActivity = now - effectiveTimeout / 2;
3148
- }
3149
- } else {
3150
- // For non-keep-alive or after warning, close the connection
3151
- console.log(
3152
- `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
3153
- `for ${plugins.prettyMs(inactivityTime)}.` +
3154
- (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
3155
- );
3156
- this.cleanupConnection(record, 'inactivity');
3157
- }
1746
+ // For non-keep-alive or after warning, close the connection
1747
+ console.log(
1748
+ `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
1749
+ `for ${plugins.prettyMs(inactivityTime)}.` +
1750
+ (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
1751
+ );
1752
+ this.cleanupConnection(record, 'inactivity');
3158
1753
  }
3159
1754
  } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
3160
1755
  // If activity detected after warning, clear the warning
3161
1756
  if (this.settings.enableDetailedLogging) {
3162
- console.log(
3163
- `[${id}] Connection activity detected after inactivity warning, resetting warning`
3164
- );
1757
+ console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`);
3165
1758
  }
3166
1759
  record.inactivityWarningIssued = false;
3167
1760
  }
@@ -3210,9 +1803,6 @@ export class PortProxy {
3210
1803
  public async stop() {
3211
1804
  console.log('PortProxy shutting down...');
3212
1805
  this.isShuttingDown = true;
3213
-
3214
- // Stop the session cleanup timer
3215
- stopSessionCleanupTimer();
3216
1806
 
3217
1807
  // Stop accepting new connections
3218
1808
  const closeServerPromises: Promise<void>[] = this.netServers.map(
@@ -3313,4 +1903,4 @@ export class PortProxy {
3313
1903
 
3314
1904
  console.log('PortProxy shutdown complete.');
3315
1905
  }
3316
- }
1906
+ }