@push.rocks/smartproxy 3.31.1 → 3.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.portproxy.d.ts +18 -2
- package/dist_ts/classes.portproxy.js +688 -141
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +852 -178
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
|
|
111
126
|
}
|
|
112
127
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Configuration for TLS session cache
|
|
130
|
+
*/
|
|
131
|
+
interface ITlsSessionCacheConfig {
|
|
132
|
+
maxEntries: number; // Maximum number of entries to keep in the cache
|
|
133
|
+
expiryTime: number; // Time in ms before sessions expire (default: 24 hours)
|
|
134
|
+
cleanupInterval: number; // Interval in ms to run cleanup (default: 10 minutes)
|
|
135
|
+
enabled: boolean; // Whether session caching is enabled
|
|
136
|
+
}
|
|
116
137
|
|
|
117
|
-
//
|
|
118
|
-
|
|
138
|
+
// Default configuration for session cache
|
|
139
|
+
const DEFAULT_SESSION_CACHE_CONFIG: ITlsSessionCacheConfig = {
|
|
140
|
+
maxEntries: 10000, // Default max 10,000 entries
|
|
141
|
+
expiryTime: 24 * 60 * 60 * 1000, // 24 hours default
|
|
142
|
+
cleanupInterval: 10 * 60 * 1000, // Clean up every 10 minutes
|
|
143
|
+
enabled: true // Enabled by default
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Enhanced TLS session cache with size limits and better performance
|
|
147
|
+
class TlsSessionCache {
|
|
148
|
+
private cache = new Map<string, ITlsSessionInfo>();
|
|
149
|
+
private config: ITlsSessionCacheConfig;
|
|
150
|
+
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
151
|
+
private lastCleanupTime: number = 0;
|
|
152
|
+
private cacheStats = {
|
|
153
|
+
hits: 0,
|
|
154
|
+
misses: 0,
|
|
155
|
+
expirations: 0,
|
|
156
|
+
evictions: 0,
|
|
157
|
+
total: 0
|
|
158
|
+
};
|
|
119
159
|
|
|
120
|
-
|
|
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
|
+
}
|
|
207
503
|
}
|
|
504
|
+
|
|
505
|
+
// If we processed records but didn't find SNI or session info
|
|
506
|
+
if (recordsExamined > 0 && !result) {
|
|
507
|
+
return {
|
|
508
|
+
isResumption: false,
|
|
509
|
+
recordsExamined,
|
|
510
|
+
multipleRecords
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return result;
|
|
515
|
+
} catch (err) {
|
|
516
|
+
console.log(`Error extracting SNI: ${err}`);
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
208
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Extracts SNI information from a single TLS record
|
|
523
|
+
* This helper function processes a single complete TLS record
|
|
524
|
+
*/
|
|
525
|
+
function extractSNIFromRecord(recordBuffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
|
|
526
|
+
try {
|
|
527
|
+
// Skip the 5-byte TLS record header
|
|
209
528
|
let offset = 5;
|
|
210
|
-
|
|
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
|
/**
|
|
@@ -1179,15 +1681,46 @@ export class PortProxy {
|
|
|
1179
1681
|
// Allow if the new SNI matches existing domain config or find a new matching config
|
|
1180
1682
|
let allowed = false;
|
|
1181
1683
|
|
|
1684
|
+
// First check if the new SNI is allowed under the existing domain config
|
|
1685
|
+
// This is the preferred approach as it maintains the existing connection context
|
|
1182
1686
|
if (record.domainConfig) {
|
|
1183
1687
|
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
|
1688
|
+
|
|
1689
|
+
if (allowed) {
|
|
1690
|
+
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} allowed by existing domain config`);
|
|
1691
|
+
}
|
|
1184
1692
|
}
|
|
1185
1693
|
|
|
1694
|
+
// If not allowed by existing config, try to find an alternative domain config
|
|
1186
1695
|
if (!allowed) {
|
|
1187
|
-
|
|
1696
|
+
// First try exact match
|
|
1697
|
+
let newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
1188
1698
|
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
|
1189
1699
|
);
|
|
1190
1700
|
|
|
1701
|
+
// If no exact match, try flexible matching with domain parts (for wildcard domains)
|
|
1702
|
+
if (!newDomainConfig) {
|
|
1703
|
+
console.log(`[${connectionId}] No exact domain config match for rehandshake SNI: ${newSNI}, trying flexible matching`);
|
|
1704
|
+
|
|
1705
|
+
const domainParts = newSNI.split('.');
|
|
1706
|
+
|
|
1707
|
+
// Try matching with parent domains or wildcard patterns
|
|
1708
|
+
if (domainParts.length > 2) {
|
|
1709
|
+
const parentDomain = domainParts.slice(1).join('.');
|
|
1710
|
+
const wildcardDomain = '*.' + parentDomain;
|
|
1711
|
+
|
|
1712
|
+
console.log(`[${connectionId}] Trying alternative patterns: ${parentDomain} or ${wildcardDomain}`);
|
|
1713
|
+
|
|
1714
|
+
newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
1715
|
+
config.domains.some((d) =>
|
|
1716
|
+
d === parentDomain ||
|
|
1717
|
+
d === wildcardDomain ||
|
|
1718
|
+
plugins.minimatch(parentDomain, d)
|
|
1719
|
+
)
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1191
1724
|
if (newDomainConfig) {
|
|
1192
1725
|
const effectiveAllowedIPs = [
|
|
1193
1726
|
...newDomainConfig.allowedIPs,
|
|
@@ -1430,7 +1963,8 @@ export class PortProxy {
|
|
|
1430
1963
|
}
|
|
1431
1964
|
|
|
1432
1965
|
/**
|
|
1433
|
-
* Update connection activity timestamp with sleep detection
|
|
1966
|
+
* Update connection activity timestamp with enhanced sleep detection
|
|
1967
|
+
* Improved for chained proxy scenarios and more aggressive handling of stale connections
|
|
1434
1968
|
*/
|
|
1435
1969
|
private updateActivity(record: IConnectionRecord): void {
|
|
1436
1970
|
// Get the current time
|
|
@@ -1440,62 +1974,107 @@ export class PortProxy {
|
|
|
1440
1974
|
if (record.lastActivity > 0) {
|
|
1441
1975
|
const timeDiff = now - record.lastActivity;
|
|
1442
1976
|
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
if (record.isTLS && record.
|
|
1456
|
-
//
|
|
1457
|
-
|
|
1458
|
-
|
|
1977
|
+
// Enhanced sleep detection with graduated thresholds
|
|
1978
|
+
// For chained proxies, we need to be more aggressive about refreshing connections
|
|
1979
|
+
const isChainedProxy = this.settings.targetIP === 'localhost' || this.settings.targetIP === '127.0.0.1';
|
|
1980
|
+
const minuteInMs = 60 * 1000;
|
|
1981
|
+
|
|
1982
|
+
// Different thresholds based on connection type and configuration
|
|
1983
|
+
const shortInactivityThreshold = isChainedProxy ? 10 * minuteInMs : 15 * minuteInMs;
|
|
1984
|
+
const mediumInactivityThreshold = isChainedProxy ? 20 * minuteInMs : 30 * minuteInMs;
|
|
1985
|
+
const longInactivityThreshold = isChainedProxy ? 60 * minuteInMs : 120 * minuteInMs;
|
|
1986
|
+
|
|
1987
|
+
// Short inactivity (10-15 mins) - Might be temporary network issue or short sleep
|
|
1988
|
+
if (timeDiff > shortInactivityThreshold) {
|
|
1989
|
+
if (record.isTLS && !record.possibleSystemSleep) {
|
|
1990
|
+
// Record first detection of possible sleep/inactivity
|
|
1991
|
+
record.possibleSystemSleep = true;
|
|
1992
|
+
record.lastSleepDetection = now;
|
|
1993
|
+
|
|
1994
|
+
if (this.settings.enableDetailedLogging) {
|
|
1459
1995
|
console.log(
|
|
1460
|
-
`[${record.id}]
|
|
1461
|
-
|
|
1996
|
+
`[${record.id}] Detected possible short inactivity for ${plugins.prettyMs(timeDiff)}. ` +
|
|
1997
|
+
`Monitoring for TLS connection health.`
|
|
1462
1998
|
);
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
// For TLS connections, send a minimal probe to check connection health
|
|
2002
|
+
if (!record.usingNetworkProxy && record.outgoing && !record.outgoing.destroyed) {
|
|
2003
|
+
try {
|
|
2004
|
+
record.outgoing.write(Buffer.alloc(0));
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
console.log(`[${record.id}] Error sending TLS probe: ${err}`);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// Medium inactivity (20-30 mins) - Likely a sleep event or network change
|
|
2013
|
+
if (timeDiff > mediumInactivityThreshold && record.hasKeepAlive) {
|
|
2014
|
+
console.log(
|
|
2015
|
+
`[${record.id}] Detected medium inactivity period for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2016
|
+
`Taking proactive steps for connection health.`
|
|
2017
|
+
);
|
|
2018
|
+
|
|
2019
|
+
// For TLS connections, we need more aggressive handling
|
|
2020
|
+
if (record.isTLS && record.tlsHandshakeComplete) {
|
|
2021
|
+
// If in a chained proxy, we should be even more aggressive about refreshing
|
|
2022
|
+
if (isChainedProxy) {
|
|
1466
2023
|
console.log(
|
|
1467
|
-
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
1468
|
-
|
|
2024
|
+
`[${record.id}] TLS connection in chained proxy inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2025
|
+
`Closing to prevent certificate inconsistencies across chain.`
|
|
1469
2026
|
);
|
|
1470
|
-
this.
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
2027
|
+
return this.initiateCleanupOnce(record, 'chained_proxy_inactivity');
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// For TLS in single proxy, try refresh first
|
|
2031
|
+
console.log(
|
|
2032
|
+
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2033
|
+
`Attempting active refresh of TLS state.`
|
|
2034
|
+
);
|
|
2035
|
+
|
|
2036
|
+
// Attempt deep TLS state refresh with buffer flush
|
|
2037
|
+
this.performDeepTlsRefresh(record);
|
|
2038
|
+
|
|
2039
|
+
// Schedule verification check with tighter timing for chained setups
|
|
2040
|
+
const verificationTimeout = isChainedProxy ? 5 * minuteInMs : 10 * minuteInMs;
|
|
2041
|
+
const refreshCheckId = record.id;
|
|
2042
|
+
const refreshCheck = setTimeout(() => {
|
|
2043
|
+
const currentRecord = this.connectionRecords.get(refreshCheckId);
|
|
2044
|
+
if (currentRecord) {
|
|
2045
|
+
const verificationTimeDiff = Date.now() - currentRecord.lastActivity;
|
|
2046
|
+
if (verificationTimeDiff > verificationTimeout / 2) {
|
|
1477
2047
|
console.log(
|
|
1478
|
-
`[${refreshCheckId}] No activity detected after TLS refresh. ` +
|
|
1479
|
-
|
|
2048
|
+
`[${refreshCheckId}] No activity detected after TLS refresh (${plugins.prettyMs(verificationTimeDiff)}). ` +
|
|
2049
|
+
`Closing connection to ensure proper browser reconnection.`
|
|
1480
2050
|
);
|
|
1481
2051
|
this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
|
|
1482
2052
|
}
|
|
1483
|
-
}, 15 * 60 * 1000);
|
|
1484
|
-
|
|
1485
|
-
// Make sure timeout doesn't keep the process alive
|
|
1486
|
-
if (refreshCheck.unref) {
|
|
1487
|
-
refreshCheck.unref();
|
|
1488
2053
|
}
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
|
|
2054
|
+
}, verificationTimeout);
|
|
2055
|
+
|
|
2056
|
+
// Make sure timeout doesn't keep the process alive
|
|
2057
|
+
if (refreshCheck.unref) {
|
|
2058
|
+
refreshCheck.unref();
|
|
1492
2059
|
}
|
|
1493
2060
|
}
|
|
1494
2061
|
|
|
1495
|
-
//
|
|
2062
|
+
// Update sleep detection markers
|
|
1496
2063
|
record.possibleSystemSleep = true;
|
|
1497
2064
|
record.lastSleepDetection = now;
|
|
1498
2065
|
}
|
|
2066
|
+
|
|
2067
|
+
// Long inactivity (60-120 mins) - Definite sleep/suspend or major network change
|
|
2068
|
+
if (timeDiff > longInactivityThreshold) {
|
|
2069
|
+
console.log(
|
|
2070
|
+
`[${record.id}] Detected long inactivity period of ${plugins.prettyMs(timeDiff)}. ` +
|
|
2071
|
+
`Closing connection to ensure fresh certificate context.`
|
|
2072
|
+
);
|
|
2073
|
+
|
|
2074
|
+
// For long periods, we always want to force close and let browser reconnect
|
|
2075
|
+
// This ensures fresh certificates and proper TLS context across the chain
|
|
2076
|
+
return this.initiateCleanupOnce(record, 'extended_inactivity_refresh');
|
|
2077
|
+
}
|
|
1499
2078
|
}
|
|
1500
2079
|
|
|
1501
2080
|
// Update the activity timestamp
|
|
@@ -1508,9 +2087,11 @@ export class PortProxy {
|
|
|
1508
2087
|
}
|
|
1509
2088
|
|
|
1510
2089
|
/**
|
|
1511
|
-
*
|
|
2090
|
+
* Perform deep TLS state refresh after sleep detection
|
|
2091
|
+
* More aggressive than the standard refresh, specifically designed for
|
|
2092
|
+
* recovering connections after system sleep in chained proxy setups
|
|
1512
2093
|
*/
|
|
1513
|
-
private
|
|
2094
|
+
private performDeepTlsRefresh(record: IConnectionRecord): void {
|
|
1514
2095
|
// Skip if we're using a NetworkProxy as it handles its own TLS state
|
|
1515
2096
|
if (record.usingNetworkProxy) {
|
|
1516
2097
|
return;
|
|
@@ -1523,31 +2104,76 @@ export class PortProxy {
|
|
|
1523
2104
|
const connectionAge = Date.now() - record.incomingStartTime;
|
|
1524
2105
|
const hourInMs = 60 * 60 * 1000;
|
|
1525
2106
|
|
|
1526
|
-
// For
|
|
1527
|
-
//
|
|
1528
|
-
if (record.isTLS && record.hasKeepAlive && connectionAge > 8 * hourInMs) { // 8 hours instead of 45 minutes
|
|
2107
|
+
// For very long-lived connections, just close them
|
|
2108
|
+
if (connectionAge > 4 * hourInMs) { // Reduced from 8 hours to 4 hours for chained proxies
|
|
1529
2109
|
console.log(
|
|
1530
2110
|
`[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
|
|
1531
|
-
|
|
2111
|
+
`Closing to ensure proper certificate handling across proxy chain.`
|
|
1532
2112
|
);
|
|
1533
|
-
return this.initiateCleanupOnce(record, '
|
|
2113
|
+
return this.initiateCleanupOnce(record, 'certificate_age_refresh');
|
|
1534
2114
|
}
|
|
1535
2115
|
|
|
1536
|
-
//
|
|
2116
|
+
// Perform a series of actions to try to refresh the TLS state
|
|
2117
|
+
|
|
2118
|
+
// 1. Send a zero-length buffer to trigger any pending errors
|
|
1537
2119
|
record.outgoing.write(Buffer.alloc(0));
|
|
2120
|
+
|
|
2121
|
+
// 2. Check socket state
|
|
2122
|
+
if (record.outgoing.writableEnded || !record.outgoing.writable) {
|
|
2123
|
+
console.log(`[${record.id}] Socket no longer writable during refresh`);
|
|
2124
|
+
return this.initiateCleanupOnce(record, 'socket_state_error');
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// 3. For TLS connections, try to force background renegotiation
|
|
2128
|
+
// by manipulating socket timeouts
|
|
2129
|
+
const originalTimeout = record.outgoing.timeout;
|
|
2130
|
+
record.outgoing.setTimeout(100); // Set very short timeout
|
|
2131
|
+
|
|
2132
|
+
// 4. Create a small delay to allow timeout to process
|
|
2133
|
+
setTimeout(() => {
|
|
2134
|
+
try {
|
|
2135
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
2136
|
+
// Reset timeout to original value
|
|
2137
|
+
record.outgoing.setTimeout(originalTimeout || 0);
|
|
2138
|
+
|
|
2139
|
+
// Send another probe with random data (16 bytes) that will be ignored by TLS layer
|
|
2140
|
+
// but might trigger internal state updates in the TLS implementation
|
|
2141
|
+
const probeBuffer = Buffer.alloc(16);
|
|
2142
|
+
// Fill with random data
|
|
2143
|
+
for (let i = 0; i < 16; i++) {
|
|
2144
|
+
probeBuffer[i] = Math.floor(Math.random() * 256);
|
|
2145
|
+
}
|
|
2146
|
+
record.outgoing.write(Buffer.alloc(0));
|
|
2147
|
+
|
|
2148
|
+
if (this.settings.enableDetailedLogging) {
|
|
2149
|
+
console.log(`[${record.id}] Completed deep TLS refresh sequence`);
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
} catch (innerErr) {
|
|
2153
|
+
console.log(`[${record.id}] Error during deep TLS refresh: ${innerErr}`);
|
|
2154
|
+
this.initiateCleanupOnce(record, 'deep_refresh_error');
|
|
2155
|
+
}
|
|
2156
|
+
}, 150);
|
|
1538
2157
|
|
|
1539
2158
|
if (this.settings.enableDetailedLogging) {
|
|
1540
|
-
console.log(`[${record.id}]
|
|
2159
|
+
console.log(`[${record.id}] Initiated deep TLS refresh sequence`);
|
|
1541
2160
|
}
|
|
1542
2161
|
}
|
|
1543
2162
|
} catch (err) {
|
|
1544
|
-
console.log(`[${record.id}] Error
|
|
2163
|
+
console.log(`[${record.id}] Error starting TLS state refresh: ${err}`);
|
|
1545
2164
|
|
|
1546
2165
|
// If we hit an error, it's likely the connection is already broken
|
|
1547
2166
|
// Force cleanup to ensure browser reconnects cleanly
|
|
1548
2167
|
return this.initiateCleanupOnce(record, 'tls_refresh_error');
|
|
1549
2168
|
}
|
|
1550
2169
|
}
|
|
2170
|
+
|
|
2171
|
+
/**
|
|
2172
|
+
* Legacy refresh method for backward compatibility
|
|
2173
|
+
*/
|
|
2174
|
+
private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
|
|
2175
|
+
return this.performDeepTlsRefresh(record);
|
|
2176
|
+
}
|
|
1551
2177
|
|
|
1552
2178
|
/**
|
|
1553
2179
|
* Cleans up a connection record.
|
|
@@ -2002,15 +2628,63 @@ export class PortProxy {
|
|
|
2002
2628
|
// The resumed domain will be in serverName if this is a session resumption
|
|
2003
2629
|
if (serverName && connectionRecord.lockedDomain === serverName && serverName !== '') {
|
|
2004
2630
|
// Override domain config lookup for session resumption - crucial for certificate selection
|
|
2005
|
-
|
|
2631
|
+
|
|
2632
|
+
// First try an exact match
|
|
2633
|
+
let resumedDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
2006
2634
|
config.domains.some((d) => plugins.minimatch(serverName, d))
|
|
2007
2635
|
);
|
|
2008
2636
|
|
|
2637
|
+
// If no exact match found, try a more flexible approach using domain parts
|
|
2638
|
+
if (!resumedDomainConfig) {
|
|
2639
|
+
console.log(`[${connectionId}] No exact domain config match for resumed domain: ${serverName}, trying flexible matching`);
|
|
2640
|
+
|
|
2641
|
+
// Extract domain parts (e.g., for "sub.example.com" try matching with "*.example.com")
|
|
2642
|
+
const domainParts = serverName.split('.');
|
|
2643
|
+
|
|
2644
|
+
// Try matching with parent domains or wildcard patterns
|
|
2645
|
+
if (domainParts.length > 2) {
|
|
2646
|
+
const parentDomain = domainParts.slice(1).join('.');
|
|
2647
|
+
const wildcardDomain = '*.' + parentDomain;
|
|
2648
|
+
|
|
2649
|
+
console.log(`[${connectionId}] Trying alternative patterns: ${parentDomain} or ${wildcardDomain}`);
|
|
2650
|
+
|
|
2651
|
+
resumedDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
2652
|
+
config.domains.some((d) =>
|
|
2653
|
+
d === parentDomain ||
|
|
2654
|
+
d === wildcardDomain ||
|
|
2655
|
+
plugins.minimatch(parentDomain, d)
|
|
2656
|
+
)
|
|
2657
|
+
);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2009
2661
|
if (resumedDomainConfig) {
|
|
2010
2662
|
domainConfig = resumedDomainConfig;
|
|
2011
|
-
console.log(`[${connectionId}]
|
|
2663
|
+
console.log(`[${connectionId}] Found domain config for resumed session: ${serverName} -> ${resumedDomainConfig.domains.join(',')}`);
|
|
2012
2664
|
} else {
|
|
2013
|
-
|
|
2665
|
+
// As a fallback, use the first domain config with the same target IP if possible
|
|
2666
|
+
if (domainConfig && domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
|
|
2667
|
+
const targetIP = domainConfig.targetIPs[0];
|
|
2668
|
+
|
|
2669
|
+
const similarConfig = this.settings.domainConfigs.find((config) =>
|
|
2670
|
+
config.targetIPs && config.targetIPs.includes(targetIP)
|
|
2671
|
+
);
|
|
2672
|
+
|
|
2673
|
+
if (similarConfig && similarConfig !== domainConfig) {
|
|
2674
|
+
console.log(`[${connectionId}] Using similar domain config with matching target IP for resumed domain: ${serverName}`);
|
|
2675
|
+
domainConfig = similarConfig;
|
|
2676
|
+
} else {
|
|
2677
|
+
console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
|
|
2678
|
+
// Log available domains to help diagnose the issue
|
|
2679
|
+
console.log(`[${connectionId}] Available domains:`,
|
|
2680
|
+
this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
|
|
2681
|
+
}
|
|
2682
|
+
} else {
|
|
2683
|
+
console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
|
|
2684
|
+
// Log available domains to help diagnose the issue
|
|
2685
|
+
console.log(`[${connectionId}] Available domains:`,
|
|
2686
|
+
this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
|
|
2687
|
+
}
|
|
2014
2688
|
}
|
|
2015
2689
|
}
|
|
2016
2690
|
|