@push.rocks/smartproxy 3.31.2 → 3.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.portproxy.d.ts +18 -2
- package/dist_ts/classes.portproxy.js +629 -137
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +769 -174
package/ts/classes.portproxy.ts
CHANGED
|
@@ -56,6 +56,19 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
56
56
|
|
|
57
57
|
// NetworkProxy integration
|
|
58
58
|
networkProxies?: NetworkProxy[]; // Array of NetworkProxy instances to use for TLS termination
|
|
59
|
+
|
|
60
|
+
// New settings for chained proxy configurations and TLS handling
|
|
61
|
+
isChainedProxy?: boolean; // Whether this proxy is part of a proxy chain (detected automatically if unspecified)
|
|
62
|
+
chainPosition?: 'first' | 'middle' | 'last'; // Position in the proxy chain (affects TLS handling)
|
|
63
|
+
aggressiveTlsRefresh?: boolean; // Use more aggressive TLS refresh timeouts (default: true for chained)
|
|
64
|
+
|
|
65
|
+
// TLS session cache configuration
|
|
66
|
+
tlsSessionCache?: {
|
|
67
|
+
enabled?: boolean; // Whether to use the TLS session cache (default: true)
|
|
68
|
+
maxEntries?: number; // Maximum cache entries (default: 10000)
|
|
69
|
+
expiryTime?: number; // Session expiry time in ms (default: 24h)
|
|
70
|
+
cleanupInterval?: number; // Cache cleanup interval in ms (default: 10min)
|
|
71
|
+
};
|
|
59
72
|
}
|
|
60
73
|
|
|
61
74
|
/**
|
|
@@ -108,49 +121,254 @@ interface ITlsSessionInfo {
|
|
|
108
121
|
sessionId?: Buffer; // The TLS session ID (if available)
|
|
109
122
|
ticketId?: string; // Session ticket identifier for newer TLS versions
|
|
110
123
|
ticketTimestamp: number; // When this session was recorded
|
|
124
|
+
lastAccessed?: number; // When this session was last accessed
|
|
125
|
+
accessCount?: number; // How many times this session has been used
|
|
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
|
|
111
136
|
}
|
|
112
137
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
};
|
|
116
145
|
|
|
117
|
-
//
|
|
118
|
-
|
|
146
|
+
// Enhanced TLS session cache with size limits and better performance
|
|
147
|
+
class TlsSessionCache {
|
|
148
|
+
private cache = new Map<string, ITlsSessionInfo>();
|
|
149
|
+
private config: ITlsSessionCacheConfig;
|
|
150
|
+
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
151
|
+
private lastCleanupTime: number = 0;
|
|
152
|
+
private cacheStats = {
|
|
153
|
+
hits: 0,
|
|
154
|
+
misses: 0,
|
|
155
|
+
expirations: 0,
|
|
156
|
+
evictions: 0,
|
|
157
|
+
total: 0
|
|
158
|
+
};
|
|
119
159
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (tlsSessionCleanupTimer) {
|
|
124
|
-
clearInterval(tlsSessionCleanupTimer);
|
|
160
|
+
constructor(config?: Partial<ITlsSessionCacheConfig>) {
|
|
161
|
+
this.config = { ...DEFAULT_SESSION_CACHE_CONFIG, ...config };
|
|
162
|
+
this.startCleanupTimer();
|
|
125
163
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get a session from the cache
|
|
167
|
+
*/
|
|
168
|
+
public get(key: string): ITlsSessionInfo | undefined {
|
|
169
|
+
// Skip if cache is disabled
|
|
170
|
+
if (!this.config.enabled) return undefined;
|
|
171
|
+
|
|
172
|
+
const entry = this.cache.get(key);
|
|
173
|
+
|
|
174
|
+
if (entry) {
|
|
175
|
+
// Update access information
|
|
176
|
+
entry.lastAccessed = Date.now();
|
|
177
|
+
entry.accessCount = (entry.accessCount || 0) + 1;
|
|
178
|
+
this.cache.set(key, entry);
|
|
179
|
+
this.cacheStats.hits++;
|
|
180
|
+
return entry;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.cacheStats.misses++;
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if the cache has a key
|
|
189
|
+
*/
|
|
190
|
+
public has(key: string): boolean {
|
|
191
|
+
// Skip if cache is disabled
|
|
192
|
+
if (!this.config.enabled) return false;
|
|
193
|
+
|
|
194
|
+
const exists = this.cache.has(key);
|
|
195
|
+
if (exists) {
|
|
196
|
+
const entry = this.cache.get(key)!;
|
|
197
|
+
|
|
198
|
+
// Check if entry has expired
|
|
199
|
+
if (Date.now() - entry.ticketTimestamp > this.config.expiryTime) {
|
|
200
|
+
this.cache.delete(key);
|
|
201
|
+
this.cacheStats.expirations++;
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Update last accessed time
|
|
206
|
+
entry.lastAccessed = Date.now();
|
|
207
|
+
this.cache.set(key, entry);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return exists;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Set a session in the cache
|
|
215
|
+
*/
|
|
216
|
+
public set(key: string, value: ITlsSessionInfo): void {
|
|
217
|
+
// Skip if cache is disabled
|
|
218
|
+
if (!this.config.enabled) return;
|
|
219
|
+
|
|
220
|
+
// Ensure timestamps are set
|
|
221
|
+
const entry = {
|
|
222
|
+
...value,
|
|
223
|
+
lastAccessed: Date.now(),
|
|
224
|
+
accessCount: 0
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Check if we need to evict entries
|
|
228
|
+
if (!this.cache.has(key) && this.cache.size >= this.config.maxEntries) {
|
|
229
|
+
this.evictOldest();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
this.cache.set(key, entry);
|
|
233
|
+
this.cacheStats.total = this.cache.size;
|
|
234
|
+
|
|
235
|
+
// Run cleanup if it's been a while
|
|
236
|
+
const timeSinceCleanup = Date.now() - this.lastCleanupTime;
|
|
237
|
+
if (timeSinceCleanup > this.config.cleanupInterval * 2) {
|
|
238
|
+
this.cleanup();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Delete a session from the cache
|
|
244
|
+
*/
|
|
245
|
+
public delete(key: string): boolean {
|
|
246
|
+
return this.cache.delete(key);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Clear the entire cache
|
|
251
|
+
*/
|
|
252
|
+
public clear(): void {
|
|
253
|
+
this.cache.clear();
|
|
254
|
+
this.cacheStats.total = 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get cache statistics
|
|
259
|
+
*/
|
|
260
|
+
public getStats(): any {
|
|
261
|
+
return {
|
|
262
|
+
...this.cacheStats,
|
|
263
|
+
size: this.cache.size,
|
|
264
|
+
enabled: this.config.enabled,
|
|
265
|
+
maxEntries: this.config.maxEntries,
|
|
266
|
+
expiryTimeHours: this.config.expiryTime / (60 * 60 * 1000)
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Update cache configuration
|
|
272
|
+
*/
|
|
273
|
+
public updateConfig(config: Partial<ITlsSessionCacheConfig>): void {
|
|
274
|
+
this.config = { ...this.config, ...config };
|
|
275
|
+
|
|
276
|
+
// Restart the cleanup timer with new interval
|
|
277
|
+
this.startCleanupTimer();
|
|
278
|
+
|
|
279
|
+
// Run immediate cleanup if max entries was reduced
|
|
280
|
+
if (config.maxEntries && this.cache.size > config.maxEntries) {
|
|
281
|
+
while (this.cache.size > config.maxEntries) {
|
|
282
|
+
this.evictOldest();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Start the cleanup timer
|
|
289
|
+
*/
|
|
290
|
+
private startCleanupTimer(): void {
|
|
291
|
+
if (this.cleanupTimer) {
|
|
292
|
+
clearInterval(this.cleanupTimer);
|
|
293
|
+
this.cleanupTimer = null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!this.config.enabled) return;
|
|
297
|
+
|
|
298
|
+
this.cleanupTimer = setInterval(() => {
|
|
299
|
+
this.cleanup();
|
|
300
|
+
}, this.config.cleanupInterval);
|
|
301
|
+
|
|
302
|
+
// Make sure the interval doesn't keep the process alive
|
|
303
|
+
if (this.cleanupTimer.unref) {
|
|
304
|
+
this.cleanupTimer.unref();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Clean up expired entries
|
|
310
|
+
*/
|
|
311
|
+
private cleanup(): void {
|
|
312
|
+
this.lastCleanupTime = Date.now();
|
|
313
|
+
|
|
129
314
|
const now = Date.now();
|
|
130
|
-
|
|
315
|
+
let expiredCount = 0;
|
|
131
316
|
|
|
132
|
-
for (const [
|
|
133
|
-
if (now - info.ticketTimestamp > expiryTime) {
|
|
134
|
-
|
|
317
|
+
for (const [key, info] of this.cache.entries()) {
|
|
318
|
+
if (now - info.ticketTimestamp > this.config.expiryTime) {
|
|
319
|
+
this.cache.delete(key);
|
|
320
|
+
expiredCount++;
|
|
135
321
|
}
|
|
136
322
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
323
|
+
|
|
324
|
+
if (expiredCount > 0) {
|
|
325
|
+
this.cacheStats.expirations += expiredCount;
|
|
326
|
+
this.cacheStats.total = this.cache.size;
|
|
327
|
+
console.log(`TLS Session Cache: Cleaned up ${expiredCount} expired entries. ${this.cache.size} entries remaining.`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Evict the oldest entries when cache is full
|
|
333
|
+
*/
|
|
334
|
+
private evictOldest(): void {
|
|
335
|
+
if (this.cache.size === 0) return;
|
|
336
|
+
|
|
337
|
+
let oldestKey: string | null = null;
|
|
338
|
+
let oldestTime = Date.now();
|
|
339
|
+
|
|
340
|
+
// Strategy: Find least recently accessed entry
|
|
341
|
+
for (const [key, info] of this.cache.entries()) {
|
|
342
|
+
const lastAccess = info.lastAccessed || info.ticketTimestamp;
|
|
343
|
+
if (lastAccess < oldestTime) {
|
|
344
|
+
oldestTime = lastAccess;
|
|
345
|
+
oldestKey = key;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (oldestKey) {
|
|
350
|
+
this.cache.delete(oldestKey);
|
|
351
|
+
this.cacheStats.evictions++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Stop cleanup timer (used during shutdown)
|
|
357
|
+
*/
|
|
358
|
+
public stop(): void {
|
|
359
|
+
if (this.cleanupTimer) {
|
|
360
|
+
clearInterval(this.cleanupTimer);
|
|
361
|
+
this.cleanupTimer = null;
|
|
362
|
+
}
|
|
142
363
|
}
|
|
143
364
|
}
|
|
144
365
|
|
|
145
|
-
//
|
|
146
|
-
|
|
366
|
+
// Create the global session cache
|
|
367
|
+
const tlsSessionCache = new TlsSessionCache();
|
|
147
368
|
|
|
148
|
-
//
|
|
369
|
+
// Legacy function for backward compatibility
|
|
149
370
|
function stopSessionCleanupTimer() {
|
|
150
|
-
|
|
151
|
-
clearInterval(tlsSessionCleanupTimer);
|
|
152
|
-
tlsSessionCleanupTimer = null;
|
|
153
|
-
}
|
|
371
|
+
tlsSessionCache.stop();
|
|
154
372
|
}
|
|
155
373
|
|
|
156
374
|
/**
|
|
@@ -165,6 +383,8 @@ interface ISNIExtractResult {
|
|
|
165
383
|
isResumption: boolean; // Whether this appears to be a session resumption
|
|
166
384
|
resumedDomain?: string; // The domain associated with the session if resuming
|
|
167
385
|
partialExtract?: boolean; // Whether this was only a partial extraction (more data needed)
|
|
386
|
+
recordsExamined?: number; // Number of TLS records examined in the buffer
|
|
387
|
+
multipleRecords?: boolean; // Whether multiple TLS records were found in the buffer
|
|
168
388
|
}
|
|
169
389
|
|
|
170
390
|
/**
|
|
@@ -172,6 +392,11 @@ interface ISNIExtractResult {
|
|
|
172
392
|
* Enhanced for robustness and detailed logging.
|
|
173
393
|
* Also extracts and tracks TLS Session IDs for session resumption handling.
|
|
174
394
|
*
|
|
395
|
+
* Improved to handle:
|
|
396
|
+
* - Multiple TLS records in a single buffer
|
|
397
|
+
* - Fragmented TLS handshakes across multiple records
|
|
398
|
+
* - Partial TLS records that may continue in future chunks
|
|
399
|
+
*
|
|
175
400
|
* @param buffer - Buffer containing the TLS ClientHello.
|
|
176
401
|
* @param enableLogging - Whether to enable detailed logging.
|
|
177
402
|
* @returns An object containing SNI and session information, or undefined if parsing fails.
|
|
@@ -181,49 +406,159 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
181
406
|
// Check if buffer is too small for TLS
|
|
182
407
|
if (buffer.length < 5) {
|
|
183
408
|
if (enableLogging) console.log('Buffer too small for TLS header');
|
|
184
|
-
return
|
|
409
|
+
return {
|
|
410
|
+
isResumption: false,
|
|
411
|
+
partialExtract: true // Indicating we need more data
|
|
412
|
+
};
|
|
185
413
|
}
|
|
186
414
|
|
|
187
|
-
// Check record type (has to be handshake - 22)
|
|
415
|
+
// Check first record type (has to be handshake - 22)
|
|
188
416
|
const recordType = buffer.readUInt8(0);
|
|
189
417
|
if (recordType !== 22) {
|
|
190
418
|
if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
|
|
191
419
|
return undefined;
|
|
192
420
|
}
|
|
193
421
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
422
|
+
// Track multiple records and total records examined
|
|
423
|
+
let recordsExamined = 0;
|
|
424
|
+
let multipleRecords = false;
|
|
425
|
+
let currentPosition = 0;
|
|
426
|
+
let result: ISNIExtractResult | undefined;
|
|
198
427
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
+
};
|
|
207
512
|
}
|
|
513
|
+
|
|
514
|
+
return result;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.log(`Error extracting SNI: ${err}`);
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
208
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Extracts SNI information from a single TLS record
|
|
523
|
+
* This helper function processes a single complete TLS record
|
|
524
|
+
*/
|
|
525
|
+
function extractSNIFromRecord(recordBuffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
|
|
526
|
+
try {
|
|
527
|
+
// Skip the 5-byte TLS record header
|
|
209
528
|
let offset = 5;
|
|
210
|
-
|
|
211
|
-
|
|
529
|
+
|
|
530
|
+
// Verify this is a handshake message
|
|
531
|
+
const handshakeType = recordBuffer.readUInt8(offset);
|
|
532
|
+
if (handshakeType !== 1) { // 1 = ClientHello
|
|
212
533
|
if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
|
|
213
534
|
return undefined;
|
|
214
535
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
536
|
+
|
|
537
|
+
// Skip the 4-byte handshake header (type + 3 bytes length)
|
|
538
|
+
offset += 4;
|
|
539
|
+
|
|
540
|
+
// Check if we have at least 38 more bytes for protocol version and random
|
|
541
|
+
if (offset + 38 > recordBuffer.length) {
|
|
542
|
+
if (enableLogging) console.log('Buffer too small for handshake header');
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
|
|
218
546
|
// Client version
|
|
219
|
-
const clientMajorVersion =
|
|
220
|
-
const clientMinorVersion =
|
|
547
|
+
const clientMajorVersion = recordBuffer.readUInt8(offset);
|
|
548
|
+
const clientMinorVersion = recordBuffer.readUInt8(offset + 1);
|
|
221
549
|
if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
550
|
+
|
|
551
|
+
// Skip version and random (2 + 32 bytes)
|
|
552
|
+
offset += 2 + 32;
|
|
553
|
+
|
|
554
|
+
// Session ID
|
|
555
|
+
if (offset + 1 > recordBuffer.length) {
|
|
556
|
+
if (enableLogging) console.log('Buffer too small for session ID length');
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
225
560
|
// Extract Session ID for session resumption tracking
|
|
226
|
-
const sessionIDLength =
|
|
561
|
+
const sessionIDLength = recordBuffer.readUInt8(offset);
|
|
227
562
|
if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
|
|
228
563
|
|
|
229
564
|
// If there's a session ID, extract it
|
|
@@ -233,7 +568,12 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
233
568
|
let resumedDomain: string | undefined;
|
|
234
569
|
|
|
235
570
|
if (sessionIDLength > 0) {
|
|
236
|
-
|
|
571
|
+
if (offset + 1 + sessionIDLength > recordBuffer.length) {
|
|
572
|
+
if (enableLogging) console.log('Buffer too small for session ID data');
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
sessionId = Buffer.from(recordBuffer.slice(offset + 1, offset + 1 + sessionIDLength));
|
|
237
577
|
|
|
238
578
|
// Convert sessionId to a string key for our cache
|
|
239
579
|
sessionIdKey = sessionId.toString('hex');
|
|
@@ -254,56 +594,121 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
254
594
|
}
|
|
255
595
|
}
|
|
256
596
|
|
|
257
|
-
offset += 1 + sessionIDLength; // Skip session ID
|
|
258
|
-
|
|
597
|
+
offset += 1 + sessionIDLength; // Skip session ID length and data
|
|
598
|
+
|
|
259
599
|
// Cipher suites
|
|
260
|
-
if (offset + 2 >
|
|
600
|
+
if (offset + 2 > recordBuffer.length) {
|
|
261
601
|
if (enableLogging) console.log('Buffer too small for cipher suites length');
|
|
262
602
|
return undefined;
|
|
263
603
|
}
|
|
264
|
-
|
|
604
|
+
|
|
605
|
+
const cipherSuitesLength = recordBuffer.readUInt16BE(offset);
|
|
265
606
|
if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
|
|
266
|
-
|
|
267
|
-
|
|
607
|
+
|
|
608
|
+
if (offset + 2 + cipherSuitesLength > recordBuffer.length) {
|
|
609
|
+
if (enableLogging) console.log('Buffer too small for cipher suites data');
|
|
610
|
+
return undefined;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
offset += 2 + cipherSuitesLength; // Skip cipher suites length and data
|
|
614
|
+
|
|
268
615
|
// Compression methods
|
|
269
|
-
if (offset + 1 >
|
|
616
|
+
if (offset + 1 > recordBuffer.length) {
|
|
270
617
|
if (enableLogging) console.log('Buffer too small for compression methods length');
|
|
271
618
|
return undefined;
|
|
272
619
|
}
|
|
273
|
-
|
|
620
|
+
|
|
621
|
+
const compressionMethodsLength = recordBuffer.readUInt8(offset);
|
|
274
622
|
if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (offset + 2 > buffer.length) {
|
|
279
|
-
if (enableLogging) console.log('Buffer too small for extensions length');
|
|
623
|
+
|
|
624
|
+
if (offset + 1 + compressionMethodsLength > recordBuffer.length) {
|
|
625
|
+
if (enableLogging) console.log('Buffer too small for compression methods data');
|
|
280
626
|
return undefined;
|
|
281
627
|
}
|
|
282
|
-
|
|
628
|
+
|
|
629
|
+
offset += 1 + compressionMethodsLength; // Skip compression methods length and data
|
|
630
|
+
|
|
631
|
+
// Check if we have extensions data
|
|
632
|
+
if (offset + 2 > recordBuffer.length) {
|
|
633
|
+
if (enableLogging) console.log('No extensions data found - end of ClientHello');
|
|
634
|
+
|
|
635
|
+
// Even without SNI, we might be dealing with a session resumption
|
|
636
|
+
if (isResumption && resumedDomain) {
|
|
637
|
+
return {
|
|
638
|
+
serverName: resumedDomain, // Use the domain from previous session
|
|
639
|
+
sessionId,
|
|
640
|
+
sessionIdKey,
|
|
641
|
+
hasSessionTicket: false,
|
|
642
|
+
isResumption: true,
|
|
643
|
+
resumedDomain
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
isResumption,
|
|
649
|
+
sessionId,
|
|
650
|
+
sessionIdKey
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Extensions
|
|
655
|
+
const extensionsLength = recordBuffer.readUInt16BE(offset);
|
|
283
656
|
if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
|
|
657
|
+
|
|
284
658
|
offset += 2;
|
|
285
659
|
const extensionsEnd = offset + extensionsLength;
|
|
286
|
-
|
|
287
|
-
if (extensionsEnd >
|
|
288
|
-
if (enableLogging)
|
|
289
|
-
console.log(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
660
|
+
|
|
661
|
+
if (extensionsEnd > recordBuffer.length) {
|
|
662
|
+
if (enableLogging) {
|
|
663
|
+
console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${recordBuffer.length}`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Even without complete extensions, we might be dealing with a session resumption
|
|
667
|
+
if (isResumption && resumedDomain) {
|
|
668
|
+
return {
|
|
669
|
+
serverName: resumedDomain, // Use the domain from previous session
|
|
670
|
+
sessionId,
|
|
671
|
+
sessionIdKey,
|
|
672
|
+
hasSessionTicket: false,
|
|
673
|
+
isResumption: true,
|
|
674
|
+
resumedDomain
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return {
|
|
679
|
+
isResumption,
|
|
680
|
+
sessionId,
|
|
681
|
+
sessionIdKey,
|
|
682
|
+
partialExtract: true // Indicating we have incomplete extensions data
|
|
683
|
+
};
|
|
293
684
|
}
|
|
294
|
-
|
|
685
|
+
|
|
295
686
|
// Variables to track session tickets
|
|
296
687
|
let hasSessionTicket = false;
|
|
297
688
|
let sessionTicketId: string | undefined;
|
|
298
689
|
|
|
299
690
|
// Parse extensions
|
|
300
691
|
while (offset + 4 <= extensionsEnd) {
|
|
301
|
-
const extensionType =
|
|
302
|
-
const extensionLength =
|
|
303
|
-
|
|
304
|
-
if (enableLogging)
|
|
692
|
+
const extensionType = recordBuffer.readUInt16BE(offset);
|
|
693
|
+
const extensionLength = recordBuffer.readUInt16BE(offset + 2);
|
|
694
|
+
|
|
695
|
+
if (enableLogging) {
|
|
305
696
|
console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
|
|
306
|
-
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (offset + 4 + extensionLength > recordBuffer.length) {
|
|
700
|
+
if (enableLogging) {
|
|
701
|
+
console.log(`Extension data incomplete. Expected: ${offset + 4 + extensionLength}, Got: ${recordBuffer.length}`);
|
|
702
|
+
}
|
|
703
|
+
return {
|
|
704
|
+
isResumption,
|
|
705
|
+
sessionId,
|
|
706
|
+
sessionIdKey,
|
|
707
|
+
hasSessionTicket,
|
|
708
|
+
partialExtract: true
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
307
712
|
offset += 4;
|
|
308
713
|
|
|
309
714
|
// Check for Session Ticket extension (type 0x0023)
|
|
@@ -312,7 +717,7 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
312
717
|
|
|
313
718
|
// Extract a hash of the ticket for tracking
|
|
314
719
|
if (extensionLength > 16) { // Ensure we have enough bytes to create a meaningful ID
|
|
315
|
-
const ticketBytes =
|
|
720
|
+
const ticketBytes = recordBuffer.slice(offset, offset + Math.min(16, extensionLength));
|
|
316
721
|
sessionTicketId = ticketBytes.toString('hex');
|
|
317
722
|
|
|
318
723
|
if (enableLogging) {
|
|
@@ -332,47 +737,74 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
332
737
|
}
|
|
333
738
|
}
|
|
334
739
|
}
|
|
335
|
-
|
|
740
|
+
|
|
741
|
+
// Server Name Indication extension (type 0x0000)
|
|
336
742
|
if (extensionType === 0x0000) {
|
|
337
|
-
|
|
338
|
-
if (offset + 2 > buffer.length) {
|
|
743
|
+
if (offset + 2 > recordBuffer.length) {
|
|
339
744
|
if (enableLogging) console.log('Buffer too small for SNI list length');
|
|
340
|
-
return
|
|
745
|
+
return {
|
|
746
|
+
isResumption,
|
|
747
|
+
sessionId,
|
|
748
|
+
sessionIdKey,
|
|
749
|
+
hasSessionTicket,
|
|
750
|
+
partialExtract: true
|
|
751
|
+
};
|
|
341
752
|
}
|
|
342
|
-
|
|
343
|
-
const sniListLength =
|
|
753
|
+
|
|
754
|
+
const sniListLength = recordBuffer.readUInt16BE(offset);
|
|
344
755
|
if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
|
|
756
|
+
|
|
345
757
|
offset += 2;
|
|
346
758
|
const sniListEnd = offset + sniListLength;
|
|
347
|
-
|
|
348
|
-
if (sniListEnd >
|
|
349
|
-
if (enableLogging)
|
|
350
|
-
console.log(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
759
|
+
|
|
760
|
+
if (sniListEnd > recordBuffer.length) {
|
|
761
|
+
if (enableLogging) {
|
|
762
|
+
console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${recordBuffer.length}`);
|
|
763
|
+
}
|
|
764
|
+
return {
|
|
765
|
+
isResumption,
|
|
766
|
+
sessionId,
|
|
767
|
+
sessionIdKey,
|
|
768
|
+
hasSessionTicket,
|
|
769
|
+
partialExtract: true
|
|
770
|
+
};
|
|
354
771
|
}
|
|
355
|
-
|
|
772
|
+
|
|
356
773
|
while (offset + 3 < sniListEnd) {
|
|
357
|
-
const nameType =
|
|
358
|
-
|
|
774
|
+
const nameType = recordBuffer.readUInt8(offset++);
|
|
775
|
+
|
|
776
|
+
if (offset + 2 > recordBuffer.length) {
|
|
777
|
+
if (enableLogging) console.log('Buffer too small for SNI name length');
|
|
778
|
+
return {
|
|
779
|
+
isResumption,
|
|
780
|
+
sessionId,
|
|
781
|
+
sessionIdKey,
|
|
782
|
+
hasSessionTicket,
|
|
783
|
+
partialExtract: true
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const nameLen = recordBuffer.readUInt16BE(offset);
|
|
359
788
|
offset += 2;
|
|
360
|
-
|
|
789
|
+
|
|
361
790
|
if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
|
|
362
|
-
|
|
791
|
+
|
|
792
|
+
// Only process hostname entries (type 0)
|
|
363
793
|
if (nameType === 0) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
794
|
+
if (offset + nameLen > recordBuffer.length) {
|
|
795
|
+
if (enableLogging) {
|
|
796
|
+
console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${recordBuffer.length}`);
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
isResumption,
|
|
800
|
+
sessionId,
|
|
801
|
+
sessionIdKey,
|
|
802
|
+
hasSessionTicket,
|
|
803
|
+
partialExtract: true
|
|
804
|
+
};
|
|
373
805
|
}
|
|
374
|
-
|
|
375
|
-
const serverName =
|
|
806
|
+
|
|
807
|
+
const serverName = recordBuffer.toString('utf8', offset, offset + nameLen);
|
|
376
808
|
if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
|
|
377
809
|
|
|
378
810
|
// Store the session ID to domain mapping for future resumptions
|
|
@@ -412,15 +844,20 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
412
844
|
hasSessionTicket
|
|
413
845
|
};
|
|
414
846
|
}
|
|
415
|
-
|
|
847
|
+
|
|
848
|
+
// Skip this name entry
|
|
416
849
|
offset += nameLen;
|
|
417
850
|
}
|
|
851
|
+
|
|
852
|
+
// Finished processing the SNI extension without finding a hostname
|
|
418
853
|
break;
|
|
419
854
|
} else {
|
|
855
|
+
// Skip other extensions
|
|
420
856
|
offset += extensionLength;
|
|
421
857
|
}
|
|
422
858
|
}
|
|
423
|
-
|
|
859
|
+
|
|
860
|
+
// We finished processing all extensions without finding SNI
|
|
424
861
|
if (enableLogging) console.log('No SNI extension found');
|
|
425
862
|
|
|
426
863
|
// Even without SNI, we might be dealing with a session resumption
|
|
@@ -446,7 +883,7 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
446
883
|
resumedDomain
|
|
447
884
|
};
|
|
448
885
|
} catch (err) {
|
|
449
|
-
console.log(`Error
|
|
886
|
+
console.log(`Error in extractSNIFromRecord: ${err}`);
|
|
450
887
|
return undefined;
|
|
451
888
|
}
|
|
452
889
|
}
|
|
@@ -564,17 +1001,71 @@ export class PortProxy {
|
|
|
564
1001
|
private networkProxies: NetworkProxy[] = [];
|
|
565
1002
|
|
|
566
1003
|
constructor(settingsArg: IPortProxySettings) {
|
|
567
|
-
//
|
|
1004
|
+
// Auto-detect if this is a chained proxy based on targetIP
|
|
1005
|
+
const targetIP = settingsArg.targetIP || 'localhost';
|
|
1006
|
+
const isChainedProxy = settingsArg.isChainedProxy !== undefined
|
|
1007
|
+
? settingsArg.isChainedProxy
|
|
1008
|
+
: (targetIP === 'localhost' || targetIP === '127.0.0.1');
|
|
1009
|
+
|
|
1010
|
+
// Use more aggressive timeouts for chained proxies
|
|
1011
|
+
const aggressiveTlsRefresh = settingsArg.aggressiveTlsRefresh !== undefined
|
|
1012
|
+
? settingsArg.aggressiveTlsRefresh
|
|
1013
|
+
: isChainedProxy;
|
|
1014
|
+
|
|
1015
|
+
// Configure TLS session cache if specified
|
|
1016
|
+
if (settingsArg.tlsSessionCache) {
|
|
1017
|
+
tlsSessionCache.updateConfig({
|
|
1018
|
+
enabled: settingsArg.tlsSessionCache.enabled,
|
|
1019
|
+
maxEntries: settingsArg.tlsSessionCache.maxEntries,
|
|
1020
|
+
expiryTime: settingsArg.tlsSessionCache.expiryTime,
|
|
1021
|
+
cleanupInterval: settingsArg.tlsSessionCache.cleanupInterval
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
console.log(`Configured TLS session cache with custom settings. Current stats: ${JSON.stringify(tlsSessionCache.getStats())}`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Determine appropriate timeouts based on proxy chain position
|
|
1028
|
+
let socketTimeout = 1800000; // 30 minutes default
|
|
1029
|
+
|
|
1030
|
+
if (isChainedProxy) {
|
|
1031
|
+
// Use shorter timeouts for chained proxies to prevent certificate issues
|
|
1032
|
+
const chainPosition = settingsArg.chainPosition || 'middle';
|
|
1033
|
+
|
|
1034
|
+
// Adjust timeouts based on position in chain
|
|
1035
|
+
switch (chainPosition) {
|
|
1036
|
+
case 'first':
|
|
1037
|
+
// First proxy can be a bit more lenient as it handles browser connections
|
|
1038
|
+
socketTimeout = 1500000; // 25 minutes
|
|
1039
|
+
break;
|
|
1040
|
+
case 'middle':
|
|
1041
|
+
// Middle proxies need shorter timeouts
|
|
1042
|
+
socketTimeout = 1200000; // 20 minutes
|
|
1043
|
+
break;
|
|
1044
|
+
case 'last':
|
|
1045
|
+
// Last proxy directly connects to backend
|
|
1046
|
+
socketTimeout = 1800000; // 30 minutes
|
|
1047
|
+
break;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
console.log(`Configured as ${chainPosition} proxy in chain. Using adjusted timeouts for optimal TLS handling.`);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Set hardcoded sensible defaults for all settings with chain-aware adjustments
|
|
568
1054
|
this.settings = {
|
|
569
1055
|
...settingsArg,
|
|
570
|
-
targetIP:
|
|
1056
|
+
targetIP: targetIP,
|
|
1057
|
+
|
|
1058
|
+
// Record the chained proxy status for use in other methods
|
|
1059
|
+
isChainedProxy: isChainedProxy,
|
|
1060
|
+
chainPosition: settingsArg.chainPosition || (isChainedProxy ? 'middle' : 'last'),
|
|
1061
|
+
aggressiveTlsRefresh: aggressiveTlsRefresh,
|
|
571
1062
|
|
|
572
1063
|
// Hardcoded timeout settings optimized for TLS safety in all deployment scenarios
|
|
573
1064
|
initialDataTimeout: 60000, // 60 seconds for initial handshake
|
|
574
|
-
socketTimeout:
|
|
575
|
-
inactivityCheckInterval: 60000, //
|
|
576
|
-
maxConnectionLifetime: 3600000, //
|
|
577
|
-
inactivityTimeout: 1800000, //
|
|
1065
|
+
socketTimeout: socketTimeout, // Adjusted based on chain position
|
|
1066
|
+
inactivityCheckInterval: isChainedProxy ? 30000 : 60000, // More frequent checks for chains
|
|
1067
|
+
maxConnectionLifetime: isChainedProxy ? 2700000 : 3600000, // 45min or 1hr lifetime
|
|
1068
|
+
inactivityTimeout: isChainedProxy ? 1200000 : 1800000, // 20min or 30min inactivity timeout
|
|
578
1069
|
|
|
579
1070
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
|
|
580
1071
|
|
|
@@ -598,11 +1089,22 @@ export class PortProxy {
|
|
|
598
1089
|
// Keep-alive settings with sensible defaults that ensure certificate safety
|
|
599
1090
|
keepAliveTreatment: 'standard', // Always use standard treatment for certificate safety
|
|
600
1091
|
keepAliveInactivityMultiplier: 2, // 2x normal inactivity timeout for minimal extension
|
|
601
|
-
|
|
1092
|
+
// Use shorter lifetime for chained proxies
|
|
1093
|
+
extendedKeepAliveLifetime: isChainedProxy
|
|
1094
|
+
? 2 * 60 * 60 * 1000 // 2 hours for chained proxies
|
|
1095
|
+
: 3 * 60 * 60 * 1000, // 3 hours for standalone proxies
|
|
602
1096
|
};
|
|
603
1097
|
|
|
604
1098
|
// Store NetworkProxy instances if provided
|
|
605
1099
|
this.networkProxies = settingsArg.networkProxies || [];
|
|
1100
|
+
|
|
1101
|
+
// Log proxy configuration details
|
|
1102
|
+
console.log(`PortProxy initialized with ${isChainedProxy ? 'chained proxy' : 'standalone'} configuration.`);
|
|
1103
|
+
if (isChainedProxy) {
|
|
1104
|
+
console.log(`TLS certificate refresh: ${aggressiveTlsRefresh ? 'Aggressive' : 'Standard'}`);
|
|
1105
|
+
console.log(`Connection lifetime: ${plugins.prettyMs(this.settings.maxConnectionLifetime)}`);
|
|
1106
|
+
console.log(`Inactivity timeout: ${plugins.prettyMs(this.settings.inactivityTimeout)}`);
|
|
1107
|
+
}
|
|
606
1108
|
}
|
|
607
1109
|
|
|
608
1110
|
/**
|
|
@@ -1461,7 +1963,8 @@ export class PortProxy {
|
|
|
1461
1963
|
}
|
|
1462
1964
|
|
|
1463
1965
|
/**
|
|
1464
|
-
* Update connection activity timestamp with sleep detection
|
|
1966
|
+
* Update connection activity timestamp with enhanced sleep detection
|
|
1967
|
+
* Improved for chained proxy scenarios and more aggressive handling of stale connections
|
|
1465
1968
|
*/
|
|
1466
1969
|
private updateActivity(record: IConnectionRecord): void {
|
|
1467
1970
|
// Get the current time
|
|
@@ -1471,62 +1974,107 @@ export class PortProxy {
|
|
|
1471
1974
|
if (record.lastActivity > 0) {
|
|
1472
1975
|
const timeDiff = now - record.lastActivity;
|
|
1473
1976
|
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
if (record.isTLS && record.
|
|
1487
|
-
//
|
|
1488
|
-
|
|
1489
|
-
|
|
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) {
|
|
1490
1995
|
console.log(
|
|
1491
|
-
`[${record.id}]
|
|
1492
|
-
|
|
1996
|
+
`[${record.id}] Detected possible short inactivity for ${plugins.prettyMs(timeDiff)}. ` +
|
|
1997
|
+
`Monitoring for TLS connection health.`
|
|
1493
1998
|
);
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
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) {
|
|
1497
2023
|
console.log(
|
|
1498
|
-
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
1499
|
-
|
|
2024
|
+
`[${record.id}] TLS connection in chained proxy inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2025
|
+
`Closing to prevent certificate inconsistencies across chain.`
|
|
1500
2026
|
);
|
|
1501
|
-
this.
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
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) {
|
|
1508
2047
|
console.log(
|
|
1509
|
-
`[${refreshCheckId}] No activity detected after TLS refresh. ` +
|
|
1510
|
-
|
|
2048
|
+
`[${refreshCheckId}] No activity detected after TLS refresh (${plugins.prettyMs(verificationTimeDiff)}). ` +
|
|
2049
|
+
`Closing connection to ensure proper browser reconnection.`
|
|
1511
2050
|
);
|
|
1512
2051
|
this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
|
|
1513
2052
|
}
|
|
1514
|
-
}, 15 * 60 * 1000);
|
|
1515
|
-
|
|
1516
|
-
// Make sure timeout doesn't keep the process alive
|
|
1517
|
-
if (refreshCheck.unref) {
|
|
1518
|
-
refreshCheck.unref();
|
|
1519
2053
|
}
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
|
|
2054
|
+
}, verificationTimeout);
|
|
2055
|
+
|
|
2056
|
+
// Make sure timeout doesn't keep the process alive
|
|
2057
|
+
if (refreshCheck.unref) {
|
|
2058
|
+
refreshCheck.unref();
|
|
1523
2059
|
}
|
|
1524
2060
|
}
|
|
1525
2061
|
|
|
1526
|
-
//
|
|
2062
|
+
// Update sleep detection markers
|
|
1527
2063
|
record.possibleSystemSleep = true;
|
|
1528
2064
|
record.lastSleepDetection = now;
|
|
1529
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
|
+
}
|
|
1530
2078
|
}
|
|
1531
2079
|
|
|
1532
2080
|
// Update the activity timestamp
|
|
@@ -1539,9 +2087,11 @@ export class PortProxy {
|
|
|
1539
2087
|
}
|
|
1540
2088
|
|
|
1541
2089
|
/**
|
|
1542
|
-
*
|
|
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
|
|
1543
2093
|
*/
|
|
1544
|
-
private
|
|
2094
|
+
private performDeepTlsRefresh(record: IConnectionRecord): void {
|
|
1545
2095
|
// Skip if we're using a NetworkProxy as it handles its own TLS state
|
|
1546
2096
|
if (record.usingNetworkProxy) {
|
|
1547
2097
|
return;
|
|
@@ -1554,31 +2104,76 @@ export class PortProxy {
|
|
|
1554
2104
|
const connectionAge = Date.now() - record.incomingStartTime;
|
|
1555
2105
|
const hourInMs = 60 * 60 * 1000;
|
|
1556
2106
|
|
|
1557
|
-
// For
|
|
1558
|
-
//
|
|
1559
|
-
if (record.isTLS && record.hasKeepAlive && connectionAge > 8 * hourInMs) { // 8 hours instead of 45 minutes
|
|
2107
|
+
// For very long-lived connections, just close them
|
|
2108
|
+
if (connectionAge > 4 * hourInMs) { // Reduced from 8 hours to 4 hours for chained proxies
|
|
1560
2109
|
console.log(
|
|
1561
2110
|
`[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
|
|
1562
|
-
|
|
2111
|
+
`Closing to ensure proper certificate handling across proxy chain.`
|
|
1563
2112
|
);
|
|
1564
|
-
return this.initiateCleanupOnce(record, '
|
|
2113
|
+
return this.initiateCleanupOnce(record, 'certificate_age_refresh');
|
|
1565
2114
|
}
|
|
1566
2115
|
|
|
1567
|
-
//
|
|
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
|
|
1568
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);
|
|
1569
2157
|
|
|
1570
2158
|
if (this.settings.enableDetailedLogging) {
|
|
1571
|
-
console.log(`[${record.id}]
|
|
2159
|
+
console.log(`[${record.id}] Initiated deep TLS refresh sequence`);
|
|
1572
2160
|
}
|
|
1573
2161
|
}
|
|
1574
2162
|
} catch (err) {
|
|
1575
|
-
console.log(`[${record.id}] Error
|
|
2163
|
+
console.log(`[${record.id}] Error starting TLS state refresh: ${err}`);
|
|
1576
2164
|
|
|
1577
2165
|
// If we hit an error, it's likely the connection is already broken
|
|
1578
2166
|
// Force cleanup to ensure browser reconnects cleanly
|
|
1579
2167
|
return this.initiateCleanupOnce(record, 'tls_refresh_error');
|
|
1580
2168
|
}
|
|
1581
2169
|
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Legacy refresh method for backward compatibility
|
|
2173
|
+
*/
|
|
2174
|
+
private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
|
|
2175
|
+
return this.performDeepTlsRefresh(record);
|
|
2176
|
+
}
|
|
1582
2177
|
|
|
1583
2178
|
/**
|
|
1584
2179
|
* Cleans up a connection record.
|