@push.rocks/smartproxy 3.31.2 → 3.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.portproxy.d.ts +18 -2
- package/dist_ts/classes.portproxy.js +853 -163
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +1025 -202
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
|
/**
|
|
@@ -87,6 +100,8 @@ interface IConnectionRecord {
|
|
|
87
100
|
|
|
88
101
|
// Keep-alive tracking
|
|
89
102
|
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
|
|
103
|
+
incomingKeepAliveEnabled?: boolean; // Whether keep-alive is enabled on incoming socket
|
|
104
|
+
outgoingKeepAliveEnabled?: boolean; // Whether keep-alive is enabled on outgoing socket
|
|
90
105
|
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
|
|
91
106
|
incomingTerminationReason?: string | null; // Reason for incoming termination
|
|
92
107
|
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
|
@@ -108,49 +123,254 @@ interface ITlsSessionInfo {
|
|
|
108
123
|
sessionId?: Buffer; // The TLS session ID (if available)
|
|
109
124
|
ticketId?: string; // Session ticket identifier for newer TLS versions
|
|
110
125
|
ticketTimestamp: number; // When this session was recorded
|
|
126
|
+
lastAccessed?: number; // When this session was last accessed
|
|
127
|
+
accessCount?: number; // How many times this session has been used
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Configuration for TLS session cache
|
|
132
|
+
*/
|
|
133
|
+
interface ITlsSessionCacheConfig {
|
|
134
|
+
maxEntries: number; // Maximum number of entries to keep in the cache
|
|
135
|
+
expiryTime: number; // Time in ms before sessions expire (default: 24 hours)
|
|
136
|
+
cleanupInterval: number; // Interval in ms to run cleanup (default: 10 minutes)
|
|
137
|
+
enabled: boolean; // Whether session caching is enabled
|
|
138
|
+
}
|
|
116
139
|
|
|
117
|
-
//
|
|
118
|
-
|
|
140
|
+
// Default configuration for session cache with relaxed timeouts
|
|
141
|
+
const DEFAULT_SESSION_CACHE_CONFIG: ITlsSessionCacheConfig = {
|
|
142
|
+
maxEntries: 20000, // Default max 20,000 entries (doubled)
|
|
143
|
+
expiryTime: 7 * 24 * 60 * 60 * 1000, // 7 days default (increased from 24 hours)
|
|
144
|
+
cleanupInterval: 30 * 60 * 1000, // Clean up every 30 minutes (relaxed from 10 minutes)
|
|
145
|
+
enabled: true // Enabled by default
|
|
146
|
+
};
|
|
119
147
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
148
|
+
// Enhanced TLS session cache with size limits and better performance
|
|
149
|
+
class TlsSessionCache {
|
|
150
|
+
private cache = new Map<string, ITlsSessionInfo>();
|
|
151
|
+
private config: ITlsSessionCacheConfig;
|
|
152
|
+
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
153
|
+
private lastCleanupTime: number = 0;
|
|
154
|
+
private cacheStats = {
|
|
155
|
+
hits: 0,
|
|
156
|
+
misses: 0,
|
|
157
|
+
expirations: 0,
|
|
158
|
+
evictions: 0,
|
|
159
|
+
total: 0
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
constructor(config?: Partial<ITlsSessionCacheConfig>) {
|
|
163
|
+
this.config = { ...DEFAULT_SESSION_CACHE_CONFIG, ...config };
|
|
164
|
+
this.startCleanupTimer();
|
|
125
165
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get a session from the cache
|
|
169
|
+
*/
|
|
170
|
+
public get(key: string): ITlsSessionInfo | undefined {
|
|
171
|
+
// Skip if cache is disabled
|
|
172
|
+
if (!this.config.enabled) return undefined;
|
|
173
|
+
|
|
174
|
+
const entry = this.cache.get(key);
|
|
175
|
+
|
|
176
|
+
if (entry) {
|
|
177
|
+
// Update access information
|
|
178
|
+
entry.lastAccessed = Date.now();
|
|
179
|
+
entry.accessCount = (entry.accessCount || 0) + 1;
|
|
180
|
+
this.cache.set(key, entry);
|
|
181
|
+
this.cacheStats.hits++;
|
|
182
|
+
return entry;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.cacheStats.misses++;
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if the cache has a key
|
|
191
|
+
*/
|
|
192
|
+
public has(key: string): boolean {
|
|
193
|
+
// Skip if cache is disabled
|
|
194
|
+
if (!this.config.enabled) return false;
|
|
195
|
+
|
|
196
|
+
const exists = this.cache.has(key);
|
|
197
|
+
if (exists) {
|
|
198
|
+
const entry = this.cache.get(key)!;
|
|
199
|
+
|
|
200
|
+
// Check if entry has expired
|
|
201
|
+
if (Date.now() - entry.ticketTimestamp > this.config.expiryTime) {
|
|
202
|
+
this.cache.delete(key);
|
|
203
|
+
this.cacheStats.expirations++;
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update last accessed time
|
|
208
|
+
entry.lastAccessed = Date.now();
|
|
209
|
+
this.cache.set(key, entry);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return exists;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Set a session in the cache
|
|
217
|
+
*/
|
|
218
|
+
public set(key: string, value: ITlsSessionInfo): void {
|
|
219
|
+
// Skip if cache is disabled
|
|
220
|
+
if (!this.config.enabled) return;
|
|
221
|
+
|
|
222
|
+
// Ensure timestamps are set
|
|
223
|
+
const entry = {
|
|
224
|
+
...value,
|
|
225
|
+
lastAccessed: Date.now(),
|
|
226
|
+
accessCount: 0
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// Check if we need to evict entries
|
|
230
|
+
if (!this.cache.has(key) && this.cache.size >= this.config.maxEntries) {
|
|
231
|
+
this.evictOldest();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
this.cache.set(key, entry);
|
|
235
|
+
this.cacheStats.total = this.cache.size;
|
|
236
|
+
|
|
237
|
+
// Run cleanup if it's been a while
|
|
238
|
+
const timeSinceCleanup = Date.now() - this.lastCleanupTime;
|
|
239
|
+
if (timeSinceCleanup > this.config.cleanupInterval * 2) {
|
|
240
|
+
this.cleanup();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Delete a session from the cache
|
|
246
|
+
*/
|
|
247
|
+
public delete(key: string): boolean {
|
|
248
|
+
return this.cache.delete(key);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clear the entire cache
|
|
253
|
+
*/
|
|
254
|
+
public clear(): void {
|
|
255
|
+
this.cache.clear();
|
|
256
|
+
this.cacheStats.total = 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get cache statistics
|
|
261
|
+
*/
|
|
262
|
+
public getStats(): any {
|
|
263
|
+
return {
|
|
264
|
+
...this.cacheStats,
|
|
265
|
+
size: this.cache.size,
|
|
266
|
+
enabled: this.config.enabled,
|
|
267
|
+
maxEntries: this.config.maxEntries,
|
|
268
|
+
expiryTimeHours: this.config.expiryTime / (60 * 60 * 1000)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Update cache configuration
|
|
274
|
+
*/
|
|
275
|
+
public updateConfig(config: Partial<ITlsSessionCacheConfig>): void {
|
|
276
|
+
this.config = { ...this.config, ...config };
|
|
277
|
+
|
|
278
|
+
// Restart the cleanup timer with new interval
|
|
279
|
+
this.startCleanupTimer();
|
|
280
|
+
|
|
281
|
+
// Run immediate cleanup if max entries was reduced
|
|
282
|
+
if (config.maxEntries && this.cache.size > config.maxEntries) {
|
|
283
|
+
while (this.cache.size > config.maxEntries) {
|
|
284
|
+
this.evictOldest();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Start the cleanup timer
|
|
291
|
+
*/
|
|
292
|
+
private startCleanupTimer(): void {
|
|
293
|
+
if (this.cleanupTimer) {
|
|
294
|
+
clearInterval(this.cleanupTimer);
|
|
295
|
+
this.cleanupTimer = null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!this.config.enabled) return;
|
|
299
|
+
|
|
300
|
+
this.cleanupTimer = setInterval(() => {
|
|
301
|
+
this.cleanup();
|
|
302
|
+
}, this.config.cleanupInterval);
|
|
303
|
+
|
|
304
|
+
// Make sure the interval doesn't keep the process alive
|
|
305
|
+
if (this.cleanupTimer.unref) {
|
|
306
|
+
this.cleanupTimer.unref();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Clean up expired entries
|
|
312
|
+
*/
|
|
313
|
+
private cleanup(): void {
|
|
314
|
+
this.lastCleanupTime = Date.now();
|
|
315
|
+
|
|
129
316
|
const now = Date.now();
|
|
130
|
-
|
|
317
|
+
let expiredCount = 0;
|
|
131
318
|
|
|
132
|
-
for (const [
|
|
133
|
-
if (now - info.ticketTimestamp > expiryTime) {
|
|
134
|
-
|
|
319
|
+
for (const [key, info] of this.cache.entries()) {
|
|
320
|
+
if (now - info.ticketTimestamp > this.config.expiryTime) {
|
|
321
|
+
this.cache.delete(key);
|
|
322
|
+
expiredCount++;
|
|
135
323
|
}
|
|
136
324
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
325
|
+
|
|
326
|
+
if (expiredCount > 0) {
|
|
327
|
+
this.cacheStats.expirations += expiredCount;
|
|
328
|
+
this.cacheStats.total = this.cache.size;
|
|
329
|
+
console.log(`TLS Session Cache: Cleaned up ${expiredCount} expired entries. ${this.cache.size} entries remaining.`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Evict the oldest entries when cache is full
|
|
335
|
+
*/
|
|
336
|
+
private evictOldest(): void {
|
|
337
|
+
if (this.cache.size === 0) return;
|
|
338
|
+
|
|
339
|
+
let oldestKey: string | null = null;
|
|
340
|
+
let oldestTime = Date.now();
|
|
341
|
+
|
|
342
|
+
// Strategy: Find least recently accessed entry
|
|
343
|
+
for (const [key, info] of this.cache.entries()) {
|
|
344
|
+
const lastAccess = info.lastAccessed || info.ticketTimestamp;
|
|
345
|
+
if (lastAccess < oldestTime) {
|
|
346
|
+
oldestTime = lastAccess;
|
|
347
|
+
oldestKey = key;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (oldestKey) {
|
|
352
|
+
this.cache.delete(oldestKey);
|
|
353
|
+
this.cacheStats.evictions++;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Stop cleanup timer (used during shutdown)
|
|
359
|
+
*/
|
|
360
|
+
public stop(): void {
|
|
361
|
+
if (this.cleanupTimer) {
|
|
362
|
+
clearInterval(this.cleanupTimer);
|
|
363
|
+
this.cleanupTimer = null;
|
|
364
|
+
}
|
|
142
365
|
}
|
|
143
366
|
}
|
|
144
367
|
|
|
145
|
-
//
|
|
146
|
-
|
|
368
|
+
// Create the global session cache
|
|
369
|
+
const tlsSessionCache = new TlsSessionCache();
|
|
147
370
|
|
|
148
|
-
//
|
|
371
|
+
// Legacy function for backward compatibility
|
|
149
372
|
function stopSessionCleanupTimer() {
|
|
150
|
-
|
|
151
|
-
clearInterval(tlsSessionCleanupTimer);
|
|
152
|
-
tlsSessionCleanupTimer = null;
|
|
153
|
-
}
|
|
373
|
+
tlsSessionCache.stop();
|
|
154
374
|
}
|
|
155
375
|
|
|
156
376
|
/**
|
|
@@ -165,6 +385,8 @@ interface ISNIExtractResult {
|
|
|
165
385
|
isResumption: boolean; // Whether this appears to be a session resumption
|
|
166
386
|
resumedDomain?: string; // The domain associated with the session if resuming
|
|
167
387
|
partialExtract?: boolean; // Whether this was only a partial extraction (more data needed)
|
|
388
|
+
recordsExamined?: number; // Number of TLS records examined in the buffer
|
|
389
|
+
multipleRecords?: boolean; // Whether multiple TLS records were found in the buffer
|
|
168
390
|
}
|
|
169
391
|
|
|
170
392
|
/**
|
|
@@ -172,6 +394,11 @@ interface ISNIExtractResult {
|
|
|
172
394
|
* Enhanced for robustness and detailed logging.
|
|
173
395
|
* Also extracts and tracks TLS Session IDs for session resumption handling.
|
|
174
396
|
*
|
|
397
|
+
* Improved to handle:
|
|
398
|
+
* - Multiple TLS records in a single buffer
|
|
399
|
+
* - Fragmented TLS handshakes across multiple records
|
|
400
|
+
* - Partial TLS records that may continue in future chunks
|
|
401
|
+
*
|
|
175
402
|
* @param buffer - Buffer containing the TLS ClientHello.
|
|
176
403
|
* @param enableLogging - Whether to enable detailed logging.
|
|
177
404
|
* @returns An object containing SNI and session information, or undefined if parsing fails.
|
|
@@ -181,49 +408,159 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
181
408
|
// Check if buffer is too small for TLS
|
|
182
409
|
if (buffer.length < 5) {
|
|
183
410
|
if (enableLogging) console.log('Buffer too small for TLS header');
|
|
184
|
-
return
|
|
411
|
+
return {
|
|
412
|
+
isResumption: false,
|
|
413
|
+
partialExtract: true // Indicating we need more data
|
|
414
|
+
};
|
|
185
415
|
}
|
|
186
416
|
|
|
187
|
-
// Check record type (has to be handshake - 22)
|
|
417
|
+
// Check first record type (has to be handshake - 22)
|
|
188
418
|
const recordType = buffer.readUInt8(0);
|
|
189
419
|
if (recordType !== 22) {
|
|
190
420
|
if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
|
|
191
421
|
return undefined;
|
|
192
422
|
}
|
|
193
423
|
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
424
|
+
// Track multiple records and total records examined
|
|
425
|
+
let recordsExamined = 0;
|
|
426
|
+
let multipleRecords = false;
|
|
427
|
+
let currentPosition = 0;
|
|
428
|
+
let result: ISNIExtractResult | undefined;
|
|
198
429
|
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
430
|
+
// Process potentially multiple TLS records in the buffer
|
|
431
|
+
while (currentPosition + 5 <= buffer.length) {
|
|
432
|
+
recordsExamined++;
|
|
433
|
+
|
|
434
|
+
// Read record header
|
|
435
|
+
const currentRecordType = buffer.readUInt8(currentPosition);
|
|
436
|
+
|
|
437
|
+
// Only process handshake records (type 22)
|
|
438
|
+
if (currentRecordType !== 22) {
|
|
439
|
+
if (enableLogging) console.log(`Skipping non-handshake record at position ${currentPosition}, type: ${currentRecordType}`);
|
|
440
|
+
|
|
441
|
+
// Move to next potential record
|
|
442
|
+
if (currentPosition + 5 <= buffer.length) {
|
|
443
|
+
// Need at least 5 bytes to determine next record length
|
|
444
|
+
const nextRecordLength = buffer.readUInt16BE(currentPosition + 3);
|
|
445
|
+
currentPosition += 5 + nextRecordLength;
|
|
446
|
+
multipleRecords = true;
|
|
447
|
+
continue;
|
|
448
|
+
} else {
|
|
449
|
+
// Not enough data to determine next record
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check TLS version
|
|
455
|
+
const majorVersion = buffer.readUInt8(currentPosition + 1);
|
|
456
|
+
const minorVersion = buffer.readUInt8(currentPosition + 2);
|
|
457
|
+
if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion} at position ${currentPosition}`);
|
|
458
|
+
|
|
459
|
+
// Get record length
|
|
460
|
+
const recordLength = buffer.readUInt16BE(currentPosition + 3);
|
|
461
|
+
|
|
462
|
+
// Check if we have the complete record
|
|
463
|
+
if (currentPosition + 5 + recordLength > buffer.length) {
|
|
464
|
+
if (enableLogging) {
|
|
465
|
+
console.log(`Incomplete TLS record at position ${currentPosition}. Expected: ${currentPosition + 5 + recordLength}, Got: ${buffer.length}`);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Return partial info and signal that more data is needed
|
|
469
|
+
return {
|
|
470
|
+
isResumption: false,
|
|
471
|
+
partialExtract: true,
|
|
472
|
+
recordsExamined,
|
|
473
|
+
multipleRecords
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Process this record - extract handshake information
|
|
478
|
+
const recordResult = extractSNIFromRecord(
|
|
479
|
+
buffer.slice(currentPosition, currentPosition + 5 + recordLength),
|
|
480
|
+
enableLogging
|
|
481
|
+
);
|
|
482
|
+
|
|
483
|
+
// If we found SNI or session info in this record, store it
|
|
484
|
+
if (recordResult && (recordResult.serverName || recordResult.isResumption)) {
|
|
485
|
+
result = recordResult;
|
|
486
|
+
result.recordsExamined = recordsExamined;
|
|
487
|
+
result.multipleRecords = multipleRecords;
|
|
488
|
+
|
|
489
|
+
// Once we've found SNI or session resumption info, we can stop processing
|
|
490
|
+
// But we'll still set the multipleRecords flag to indicate more records exist
|
|
491
|
+
if (currentPosition + 5 + recordLength < buffer.length) {
|
|
492
|
+
result.multipleRecords = true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Move to the next record
|
|
499
|
+
currentPosition += 5 + recordLength;
|
|
500
|
+
|
|
501
|
+
// Set the flag if we've processed multiple records
|
|
502
|
+
if (currentPosition < buffer.length) {
|
|
503
|
+
multipleRecords = true;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// If we processed records but didn't find SNI or session info
|
|
508
|
+
if (recordsExamined > 0 && !result) {
|
|
509
|
+
return {
|
|
510
|
+
isResumption: false,
|
|
511
|
+
recordsExamined,
|
|
512
|
+
multipleRecords
|
|
513
|
+
};
|
|
207
514
|
}
|
|
515
|
+
|
|
516
|
+
return result;
|
|
517
|
+
} catch (err) {
|
|
518
|
+
console.log(`Error extracting SNI: ${err}`);
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
208
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Extracts SNI information from a single TLS record
|
|
525
|
+
* This helper function processes a single complete TLS record
|
|
526
|
+
*/
|
|
527
|
+
function extractSNIFromRecord(recordBuffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
|
|
528
|
+
try {
|
|
529
|
+
// Skip the 5-byte TLS record header
|
|
209
530
|
let offset = 5;
|
|
210
|
-
|
|
211
|
-
|
|
531
|
+
|
|
532
|
+
// Verify this is a handshake message
|
|
533
|
+
const handshakeType = recordBuffer.readUInt8(offset);
|
|
534
|
+
if (handshakeType !== 1) { // 1 = ClientHello
|
|
212
535
|
if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
|
|
213
536
|
return undefined;
|
|
214
537
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
538
|
+
|
|
539
|
+
// Skip the 4-byte handshake header (type + 3 bytes length)
|
|
540
|
+
offset += 4;
|
|
541
|
+
|
|
542
|
+
// Check if we have at least 38 more bytes for protocol version and random
|
|
543
|
+
if (offset + 38 > recordBuffer.length) {
|
|
544
|
+
if (enableLogging) console.log('Buffer too small for handshake header');
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
|
|
218
548
|
// Client version
|
|
219
|
-
const clientMajorVersion =
|
|
220
|
-
const clientMinorVersion =
|
|
549
|
+
const clientMajorVersion = recordBuffer.readUInt8(offset);
|
|
550
|
+
const clientMinorVersion = recordBuffer.readUInt8(offset + 1);
|
|
221
551
|
if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
552
|
+
|
|
553
|
+
// Skip version and random (2 + 32 bytes)
|
|
554
|
+
offset += 2 + 32;
|
|
555
|
+
|
|
556
|
+
// Session ID
|
|
557
|
+
if (offset + 1 > recordBuffer.length) {
|
|
558
|
+
if (enableLogging) console.log('Buffer too small for session ID length');
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
225
562
|
// Extract Session ID for session resumption tracking
|
|
226
|
-
const sessionIDLength =
|
|
563
|
+
const sessionIDLength = recordBuffer.readUInt8(offset);
|
|
227
564
|
if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
|
|
228
565
|
|
|
229
566
|
// If there's a session ID, extract it
|
|
@@ -233,7 +570,12 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
233
570
|
let resumedDomain: string | undefined;
|
|
234
571
|
|
|
235
572
|
if (sessionIDLength > 0) {
|
|
236
|
-
|
|
573
|
+
if (offset + 1 + sessionIDLength > recordBuffer.length) {
|
|
574
|
+
if (enableLogging) console.log('Buffer too small for session ID data');
|
|
575
|
+
return undefined;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
sessionId = Buffer.from(recordBuffer.slice(offset + 1, offset + 1 + sessionIDLength));
|
|
237
579
|
|
|
238
580
|
// Convert sessionId to a string key for our cache
|
|
239
581
|
sessionIdKey = sessionId.toString('hex');
|
|
@@ -254,56 +596,121 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
254
596
|
}
|
|
255
597
|
}
|
|
256
598
|
|
|
257
|
-
offset += 1 + sessionIDLength; // Skip session ID
|
|
258
|
-
|
|
599
|
+
offset += 1 + sessionIDLength; // Skip session ID length and data
|
|
600
|
+
|
|
259
601
|
// Cipher suites
|
|
260
|
-
if (offset + 2 >
|
|
602
|
+
if (offset + 2 > recordBuffer.length) {
|
|
261
603
|
if (enableLogging) console.log('Buffer too small for cipher suites length');
|
|
262
604
|
return undefined;
|
|
263
605
|
}
|
|
264
|
-
|
|
606
|
+
|
|
607
|
+
const cipherSuitesLength = recordBuffer.readUInt16BE(offset);
|
|
265
608
|
if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
|
|
266
|
-
|
|
267
|
-
|
|
609
|
+
|
|
610
|
+
if (offset + 2 + cipherSuitesLength > recordBuffer.length) {
|
|
611
|
+
if (enableLogging) console.log('Buffer too small for cipher suites data');
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
offset += 2 + cipherSuitesLength; // Skip cipher suites length and data
|
|
616
|
+
|
|
268
617
|
// Compression methods
|
|
269
|
-
if (offset + 1 >
|
|
618
|
+
if (offset + 1 > recordBuffer.length) {
|
|
270
619
|
if (enableLogging) console.log('Buffer too small for compression methods length');
|
|
271
620
|
return undefined;
|
|
272
621
|
}
|
|
273
|
-
|
|
622
|
+
|
|
623
|
+
const compressionMethodsLength = recordBuffer.readUInt8(offset);
|
|
274
624
|
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');
|
|
625
|
+
|
|
626
|
+
if (offset + 1 + compressionMethodsLength > recordBuffer.length) {
|
|
627
|
+
if (enableLogging) console.log('Buffer too small for compression methods data');
|
|
280
628
|
return undefined;
|
|
281
629
|
}
|
|
282
|
-
|
|
630
|
+
|
|
631
|
+
offset += 1 + compressionMethodsLength; // Skip compression methods length and data
|
|
632
|
+
|
|
633
|
+
// Check if we have extensions data
|
|
634
|
+
if (offset + 2 > recordBuffer.length) {
|
|
635
|
+
if (enableLogging) console.log('No extensions data found - end of ClientHello');
|
|
636
|
+
|
|
637
|
+
// Even without SNI, we might be dealing with a session resumption
|
|
638
|
+
if (isResumption && resumedDomain) {
|
|
639
|
+
return {
|
|
640
|
+
serverName: resumedDomain, // Use the domain from previous session
|
|
641
|
+
sessionId,
|
|
642
|
+
sessionIdKey,
|
|
643
|
+
hasSessionTicket: false,
|
|
644
|
+
isResumption: true,
|
|
645
|
+
resumedDomain
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
isResumption,
|
|
651
|
+
sessionId,
|
|
652
|
+
sessionIdKey
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Extensions
|
|
657
|
+
const extensionsLength = recordBuffer.readUInt16BE(offset);
|
|
283
658
|
if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
|
|
659
|
+
|
|
284
660
|
offset += 2;
|
|
285
661
|
const extensionsEnd = offset + extensionsLength;
|
|
286
|
-
|
|
287
|
-
if (extensionsEnd >
|
|
288
|
-
if (enableLogging)
|
|
289
|
-
console.log(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
662
|
+
|
|
663
|
+
if (extensionsEnd > recordBuffer.length) {
|
|
664
|
+
if (enableLogging) {
|
|
665
|
+
console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${recordBuffer.length}`);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Even without complete extensions, we might be dealing with a session resumption
|
|
669
|
+
if (isResumption && resumedDomain) {
|
|
670
|
+
return {
|
|
671
|
+
serverName: resumedDomain, // Use the domain from previous session
|
|
672
|
+
sessionId,
|
|
673
|
+
sessionIdKey,
|
|
674
|
+
hasSessionTicket: false,
|
|
675
|
+
isResumption: true,
|
|
676
|
+
resumedDomain
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return {
|
|
681
|
+
isResumption,
|
|
682
|
+
sessionId,
|
|
683
|
+
sessionIdKey,
|
|
684
|
+
partialExtract: true // Indicating we have incomplete extensions data
|
|
685
|
+
};
|
|
293
686
|
}
|
|
294
|
-
|
|
687
|
+
|
|
295
688
|
// Variables to track session tickets
|
|
296
689
|
let hasSessionTicket = false;
|
|
297
690
|
let sessionTicketId: string | undefined;
|
|
298
691
|
|
|
299
692
|
// Parse extensions
|
|
300
693
|
while (offset + 4 <= extensionsEnd) {
|
|
301
|
-
const extensionType =
|
|
302
|
-
const extensionLength =
|
|
303
|
-
|
|
304
|
-
if (enableLogging)
|
|
694
|
+
const extensionType = recordBuffer.readUInt16BE(offset);
|
|
695
|
+
const extensionLength = recordBuffer.readUInt16BE(offset + 2);
|
|
696
|
+
|
|
697
|
+
if (enableLogging) {
|
|
305
698
|
console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
|
|
306
|
-
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (offset + 4 + extensionLength > recordBuffer.length) {
|
|
702
|
+
if (enableLogging) {
|
|
703
|
+
console.log(`Extension data incomplete. Expected: ${offset + 4 + extensionLength}, Got: ${recordBuffer.length}`);
|
|
704
|
+
}
|
|
705
|
+
return {
|
|
706
|
+
isResumption,
|
|
707
|
+
sessionId,
|
|
708
|
+
sessionIdKey,
|
|
709
|
+
hasSessionTicket,
|
|
710
|
+
partialExtract: true
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
307
714
|
offset += 4;
|
|
308
715
|
|
|
309
716
|
// Check for Session Ticket extension (type 0x0023)
|
|
@@ -312,7 +719,7 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
312
719
|
|
|
313
720
|
// Extract a hash of the ticket for tracking
|
|
314
721
|
if (extensionLength > 16) { // Ensure we have enough bytes to create a meaningful ID
|
|
315
|
-
const ticketBytes =
|
|
722
|
+
const ticketBytes = recordBuffer.slice(offset, offset + Math.min(16, extensionLength));
|
|
316
723
|
sessionTicketId = ticketBytes.toString('hex');
|
|
317
724
|
|
|
318
725
|
if (enableLogging) {
|
|
@@ -332,47 +739,74 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
332
739
|
}
|
|
333
740
|
}
|
|
334
741
|
}
|
|
335
|
-
|
|
742
|
+
|
|
743
|
+
// Server Name Indication extension (type 0x0000)
|
|
336
744
|
if (extensionType === 0x0000) {
|
|
337
|
-
|
|
338
|
-
if (offset + 2 > buffer.length) {
|
|
745
|
+
if (offset + 2 > recordBuffer.length) {
|
|
339
746
|
if (enableLogging) console.log('Buffer too small for SNI list length');
|
|
340
|
-
return
|
|
747
|
+
return {
|
|
748
|
+
isResumption,
|
|
749
|
+
sessionId,
|
|
750
|
+
sessionIdKey,
|
|
751
|
+
hasSessionTicket,
|
|
752
|
+
partialExtract: true
|
|
753
|
+
};
|
|
341
754
|
}
|
|
342
|
-
|
|
343
|
-
const sniListLength =
|
|
755
|
+
|
|
756
|
+
const sniListLength = recordBuffer.readUInt16BE(offset);
|
|
344
757
|
if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
|
|
758
|
+
|
|
345
759
|
offset += 2;
|
|
346
760
|
const sniListEnd = offset + sniListLength;
|
|
347
|
-
|
|
348
|
-
if (sniListEnd >
|
|
349
|
-
if (enableLogging)
|
|
350
|
-
console.log(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
761
|
+
|
|
762
|
+
if (sniListEnd > recordBuffer.length) {
|
|
763
|
+
if (enableLogging) {
|
|
764
|
+
console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${recordBuffer.length}`);
|
|
765
|
+
}
|
|
766
|
+
return {
|
|
767
|
+
isResumption,
|
|
768
|
+
sessionId,
|
|
769
|
+
sessionIdKey,
|
|
770
|
+
hasSessionTicket,
|
|
771
|
+
partialExtract: true
|
|
772
|
+
};
|
|
354
773
|
}
|
|
355
|
-
|
|
774
|
+
|
|
356
775
|
while (offset + 3 < sniListEnd) {
|
|
357
|
-
const nameType =
|
|
358
|
-
|
|
776
|
+
const nameType = recordBuffer.readUInt8(offset++);
|
|
777
|
+
|
|
778
|
+
if (offset + 2 > recordBuffer.length) {
|
|
779
|
+
if (enableLogging) console.log('Buffer too small for SNI name length');
|
|
780
|
+
return {
|
|
781
|
+
isResumption,
|
|
782
|
+
sessionId,
|
|
783
|
+
sessionIdKey,
|
|
784
|
+
hasSessionTicket,
|
|
785
|
+
partialExtract: true
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const nameLen = recordBuffer.readUInt16BE(offset);
|
|
359
790
|
offset += 2;
|
|
360
|
-
|
|
791
|
+
|
|
361
792
|
if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
|
|
362
|
-
|
|
793
|
+
|
|
794
|
+
// Only process hostname entries (type 0)
|
|
363
795
|
if (nameType === 0) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
796
|
+
if (offset + nameLen > recordBuffer.length) {
|
|
797
|
+
if (enableLogging) {
|
|
798
|
+
console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${recordBuffer.length}`);
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
isResumption,
|
|
802
|
+
sessionId,
|
|
803
|
+
sessionIdKey,
|
|
804
|
+
hasSessionTicket,
|
|
805
|
+
partialExtract: true
|
|
806
|
+
};
|
|
373
807
|
}
|
|
374
|
-
|
|
375
|
-
const serverName =
|
|
808
|
+
|
|
809
|
+
const serverName = recordBuffer.toString('utf8', offset, offset + nameLen);
|
|
376
810
|
if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
|
|
377
811
|
|
|
378
812
|
// Store the session ID to domain mapping for future resumptions
|
|
@@ -412,15 +846,20 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
412
846
|
hasSessionTicket
|
|
413
847
|
};
|
|
414
848
|
}
|
|
415
|
-
|
|
849
|
+
|
|
850
|
+
// Skip this name entry
|
|
416
851
|
offset += nameLen;
|
|
417
852
|
}
|
|
853
|
+
|
|
854
|
+
// Finished processing the SNI extension without finding a hostname
|
|
418
855
|
break;
|
|
419
856
|
} else {
|
|
857
|
+
// Skip other extensions
|
|
420
858
|
offset += extensionLength;
|
|
421
859
|
}
|
|
422
860
|
}
|
|
423
|
-
|
|
861
|
+
|
|
862
|
+
// We finished processing all extensions without finding SNI
|
|
424
863
|
if (enableLogging) console.log('No SNI extension found');
|
|
425
864
|
|
|
426
865
|
// Even without SNI, we might be dealing with a session resumption
|
|
@@ -446,7 +885,7 @@ function extractSNIInfo(buffer: Buffer, enableLogging: boolean = false): ISNIExt
|
|
|
446
885
|
resumedDomain
|
|
447
886
|
};
|
|
448
887
|
} catch (err) {
|
|
449
|
-
console.log(`Error
|
|
888
|
+
console.log(`Error in extractSNIFromRecord: ${err}`);
|
|
450
889
|
return undefined;
|
|
451
890
|
}
|
|
452
891
|
}
|
|
@@ -564,45 +1003,111 @@ export class PortProxy {
|
|
|
564
1003
|
private networkProxies: NetworkProxy[] = [];
|
|
565
1004
|
|
|
566
1005
|
constructor(settingsArg: IPortProxySettings) {
|
|
567
|
-
//
|
|
1006
|
+
// Auto-detect if this is a chained proxy based on targetIP
|
|
1007
|
+
const targetIP = settingsArg.targetIP || 'localhost';
|
|
1008
|
+
const isChainedProxy = settingsArg.isChainedProxy !== undefined
|
|
1009
|
+
? settingsArg.isChainedProxy
|
|
1010
|
+
: (targetIP === 'localhost' || targetIP === '127.0.0.1');
|
|
1011
|
+
|
|
1012
|
+
// Use more aggressive timeouts for chained proxies
|
|
1013
|
+
const aggressiveTlsRefresh = settingsArg.aggressiveTlsRefresh !== undefined
|
|
1014
|
+
? settingsArg.aggressiveTlsRefresh
|
|
1015
|
+
: isChainedProxy;
|
|
1016
|
+
|
|
1017
|
+
// Configure TLS session cache if specified
|
|
1018
|
+
if (settingsArg.tlsSessionCache) {
|
|
1019
|
+
tlsSessionCache.updateConfig({
|
|
1020
|
+
enabled: settingsArg.tlsSessionCache.enabled,
|
|
1021
|
+
maxEntries: settingsArg.tlsSessionCache.maxEntries,
|
|
1022
|
+
expiryTime: settingsArg.tlsSessionCache.expiryTime,
|
|
1023
|
+
cleanupInterval: settingsArg.tlsSessionCache.cleanupInterval
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
console.log(`Configured TLS session cache with custom settings. Current stats: ${JSON.stringify(tlsSessionCache.getStats())}`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Determine appropriate timeouts based on proxy chain position
|
|
1030
|
+
// Much more relaxed socket timeouts
|
|
1031
|
+
let socketTimeout = 6 * 60 * 60 * 1000; // 6 hours default for standalone
|
|
1032
|
+
|
|
1033
|
+
if (isChainedProxy) {
|
|
1034
|
+
// Still adjust based on chain position, but with more relaxed values
|
|
1035
|
+
const chainPosition = settingsArg.chainPosition || 'middle';
|
|
1036
|
+
|
|
1037
|
+
// Adjust timeouts based on position in chain, but significantly relaxed
|
|
1038
|
+
switch (chainPosition) {
|
|
1039
|
+
case 'first':
|
|
1040
|
+
// First proxy handling browser connections
|
|
1041
|
+
socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
|
|
1042
|
+
break;
|
|
1043
|
+
case 'middle':
|
|
1044
|
+
// Middle proxies
|
|
1045
|
+
socketTimeout = 5 * 60 * 60 * 1000; // 5 hours
|
|
1046
|
+
break;
|
|
1047
|
+
case 'last':
|
|
1048
|
+
// Last proxy connects to backend
|
|
1049
|
+
socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
console.log(`Configured as ${chainPosition} proxy in chain. Using relaxed timeouts for better stability.`);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Set sensible defaults with significantly relaxed timeouts
|
|
568
1057
|
this.settings = {
|
|
569
1058
|
...settingsArg,
|
|
570
|
-
targetIP:
|
|
571
|
-
|
|
572
|
-
// Hardcoded timeout settings optimized for TLS safety in all deployment scenarios
|
|
573
|
-
initialDataTimeout: 60000, // 60 seconds for initial handshake
|
|
574
|
-
socketTimeout: 1800000, // 30 minutes - short enough for regular certificate refresh
|
|
575
|
-
inactivityCheckInterval: 60000, // 60 seconds interval for regular cleanup
|
|
576
|
-
maxConnectionLifetime: 3600000, // 1 hour maximum lifetime for all connections
|
|
577
|
-
inactivityTimeout: 1800000, // 30 minutes inactivity timeout
|
|
1059
|
+
targetIP: targetIP,
|
|
578
1060
|
|
|
579
|
-
|
|
1061
|
+
// Record the chained proxy status for use in other methods
|
|
1062
|
+
isChainedProxy: isChainedProxy,
|
|
1063
|
+
chainPosition: settingsArg.chainPosition || (isChainedProxy ? 'middle' : 'last'),
|
|
1064
|
+
aggressiveTlsRefresh: aggressiveTlsRefresh,
|
|
1065
|
+
|
|
1066
|
+
// Much more relaxed timeout settings
|
|
1067
|
+
initialDataTimeout: 120000, // 2 minutes for initial handshake (doubled)
|
|
1068
|
+
socketTimeout: socketTimeout, // 5-6 hours based on chain position
|
|
1069
|
+
inactivityCheckInterval: 5 * 60 * 1000, // 5 minutes between checks (relaxed)
|
|
1070
|
+
maxConnectionLifetime: 12 * 60 * 60 * 1000, // 12 hours lifetime
|
|
1071
|
+
inactivityTimeout: 4 * 60 * 60 * 1000, // 4 hours inactivity timeout
|
|
1072
|
+
|
|
1073
|
+
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 60000, // 60 seconds
|
|
580
1074
|
|
|
581
1075
|
// Socket optimization settings
|
|
582
1076
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
583
1077
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
584
|
-
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay ||
|
|
585
|
-
maxPendingDataSize: settingsArg.maxPendingDataSize ||
|
|
1078
|
+
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds (increased)
|
|
1079
|
+
maxPendingDataSize: settingsArg.maxPendingDataSize || 20 * 1024 * 1024, // 20MB to handle large TLS handshakes
|
|
586
1080
|
|
|
587
1081
|
// Feature flags - simplified with sensible defaults
|
|
588
|
-
disableInactivityCheck: false, //
|
|
589
|
-
enableKeepAliveProbes: true, //
|
|
1082
|
+
disableInactivityCheck: false, // Still enable inactivity checks
|
|
1083
|
+
enableKeepAliveProbes: true, // Still enable keep-alive probes
|
|
590
1084
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
591
1085
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
592
1086
|
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
|
593
1087
|
|
|
594
1088
|
// Rate limiting defaults
|
|
595
|
-
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP ||
|
|
596
|
-
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute ||
|
|
597
|
-
|
|
598
|
-
// Keep-alive settings with
|
|
599
|
-
keepAliveTreatment: '
|
|
600
|
-
keepAliveInactivityMultiplier:
|
|
601
|
-
|
|
1089
|
+
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 200, // 200 connections per IP (doubled)
|
|
1090
|
+
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 500, // 500 per minute (increased)
|
|
1091
|
+
|
|
1092
|
+
// Keep-alive settings with much more relaxed defaults
|
|
1093
|
+
keepAliveTreatment: 'extended', // Use extended keep-alive treatment
|
|
1094
|
+
keepAliveInactivityMultiplier: 3, // 3x normal inactivity timeout for longer extension
|
|
1095
|
+
// Much longer keep-alive lifetimes
|
|
1096
|
+
extendedKeepAliveLifetime: isChainedProxy
|
|
1097
|
+
? 24 * 60 * 60 * 1000 // 24 hours for chained proxies
|
|
1098
|
+
: 48 * 60 * 60 * 1000, // 48 hours for standalone proxies
|
|
602
1099
|
};
|
|
603
1100
|
|
|
604
1101
|
// Store NetworkProxy instances if provided
|
|
605
1102
|
this.networkProxies = settingsArg.networkProxies || [];
|
|
1103
|
+
|
|
1104
|
+
// Log proxy configuration details
|
|
1105
|
+
console.log(`PortProxy initialized with ${isChainedProxy ? 'chained proxy' : 'standalone'} configuration.`);
|
|
1106
|
+
if (isChainedProxy) {
|
|
1107
|
+
console.log(`TLS certificate refresh: ${aggressiveTlsRefresh ? 'Aggressive' : 'Standard'}`);
|
|
1108
|
+
console.log(`Connection lifetime: ${plugins.prettyMs(this.settings.maxConnectionLifetime)}`);
|
|
1109
|
+
console.log(`Inactivity timeout: ${plugins.prettyMs(this.settings.inactivityTimeout)}`);
|
|
1110
|
+
}
|
|
606
1111
|
}
|
|
607
1112
|
|
|
608
1113
|
/**
|
|
@@ -652,10 +1157,13 @@ export class PortProxy {
|
|
|
652
1157
|
);
|
|
653
1158
|
}
|
|
654
1159
|
|
|
655
|
-
// Create a connection to the NetworkProxy
|
|
1160
|
+
// Create a connection to the NetworkProxy with optimized settings for reliability
|
|
656
1161
|
const proxySocket = plugins.net.connect({
|
|
657
1162
|
host: proxyHost,
|
|
658
1163
|
port: proxyPort,
|
|
1164
|
+
noDelay: true, // Disable Nagle's algorithm for NetworkProxy connections
|
|
1165
|
+
keepAlive: this.settings.keepAlive, // Use the same keepAlive setting as regular connections
|
|
1166
|
+
keepAliveInitialDelay: Math.max(this.settings.keepAliveInitialDelay - 5000, 5000) // Slightly faster
|
|
659
1167
|
});
|
|
660
1168
|
|
|
661
1169
|
// Store the outgoing socket in the record
|
|
@@ -663,6 +1171,30 @@ export class PortProxy {
|
|
|
663
1171
|
record.outgoingStartTime = Date.now();
|
|
664
1172
|
record.usingNetworkProxy = true;
|
|
665
1173
|
record.networkProxyIndex = proxyIndex;
|
|
1174
|
+
|
|
1175
|
+
// Mark keep-alive as enabled on outgoing if requested
|
|
1176
|
+
if (this.settings.keepAlive) {
|
|
1177
|
+
record.outgoingKeepAliveEnabled = true;
|
|
1178
|
+
|
|
1179
|
+
// Apply enhanced TCP keep-alive options if enabled
|
|
1180
|
+
if (this.settings.enableKeepAliveProbes) {
|
|
1181
|
+
try {
|
|
1182
|
+
if ('setKeepAliveProbes' in proxySocket) {
|
|
1183
|
+
(proxySocket as any).setKeepAliveProbes(10);
|
|
1184
|
+
}
|
|
1185
|
+
if ('setKeepAliveInterval' in proxySocket) {
|
|
1186
|
+
(proxySocket as any).setKeepAliveInterval(800);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
console.log(`[${connectionId}] Enhanced TCP keep-alive configured for NetworkProxy connection`);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
// Ignore errors - these are optional enhancements
|
|
1192
|
+
if (this.settings.enableDetailedLogging) {
|
|
1193
|
+
console.log(`[${connectionId}] Enhanced keep-alive not supported for NetworkProxy: ${err}`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
666
1198
|
|
|
667
1199
|
// Set up error handlers
|
|
668
1200
|
proxySocket.on('error', (err) => {
|
|
@@ -711,6 +1243,41 @@ export class PortProxy {
|
|
|
711
1243
|
|
|
712
1244
|
// Update activity on data transfer from the proxy socket
|
|
713
1245
|
proxySocket.on('data', () => this.updateActivity(record));
|
|
1246
|
+
|
|
1247
|
+
// Special handling for application-level keep-alives on NetworkProxy connections
|
|
1248
|
+
if (this.settings.keepAlive && record.isTLS) {
|
|
1249
|
+
// Set up a timer to periodically send application-level keep-alives
|
|
1250
|
+
const keepAliveTimer = setInterval(() => {
|
|
1251
|
+
if (proxySocket && !proxySocket.destroyed && record && !record.connectionClosed) {
|
|
1252
|
+
try {
|
|
1253
|
+
// Send 0-byte packet as application-level keep-alive
|
|
1254
|
+
proxySocket.write(Buffer.alloc(0));
|
|
1255
|
+
|
|
1256
|
+
if (this.settings.enableDetailedLogging) {
|
|
1257
|
+
console.log(`[${connectionId}] Sent application-level keep-alive to NetworkProxy connection`);
|
|
1258
|
+
}
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
// If we can't write, the connection is probably already dead
|
|
1261
|
+
if (this.settings.enableDetailedLogging) {
|
|
1262
|
+
console.log(`[${connectionId}] Error sending application-level keep-alive to NetworkProxy: ${err}`);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Stop the timer if we hit an error
|
|
1266
|
+
clearInterval(keepAliveTimer);
|
|
1267
|
+
}
|
|
1268
|
+
} else {
|
|
1269
|
+
// Clean up timer if connection is gone
|
|
1270
|
+
clearInterval(keepAliveTimer);
|
|
1271
|
+
}
|
|
1272
|
+
}, 60000); // Send keep-alive every minute
|
|
1273
|
+
|
|
1274
|
+
// Make sure interval doesn't prevent process exit
|
|
1275
|
+
if (keepAliveTimer.unref) {
|
|
1276
|
+
keepAliveTimer.unref();
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
console.log(`[${connectionId}] Application-level keep-alive configured for NetworkProxy connection`);
|
|
1280
|
+
}
|
|
714
1281
|
|
|
715
1282
|
if (this.settings.enableDetailedLogging) {
|
|
716
1283
|
console.log(
|
|
@@ -832,17 +1399,25 @@ export class PortProxy {
|
|
|
832
1399
|
|
|
833
1400
|
// Apply keep-alive settings to the outgoing connection as well
|
|
834
1401
|
if (this.settings.keepAlive) {
|
|
835
|
-
|
|
1402
|
+
// Use a slightly shorter initial delay for outgoing to ensure it stays active
|
|
1403
|
+
const outgoingInitialDelay = Math.max(this.settings.keepAliveInitialDelay - 5000, 5000);
|
|
1404
|
+
targetSocket.setKeepAlive(true, outgoingInitialDelay);
|
|
1405
|
+
record.outgoingKeepAliveEnabled = true;
|
|
1406
|
+
|
|
1407
|
+
console.log(`[${connectionId}] Keep-alive enabled on outgoing connection with initial delay: ${outgoingInitialDelay}ms`);
|
|
836
1408
|
|
|
837
1409
|
// Apply enhanced TCP keep-alive options if enabled
|
|
838
1410
|
if (this.settings.enableKeepAliveProbes) {
|
|
839
1411
|
try {
|
|
840
1412
|
if ('setKeepAliveProbes' in targetSocket) {
|
|
841
|
-
(targetSocket as any).setKeepAliveProbes(10);
|
|
1413
|
+
(targetSocket as any).setKeepAliveProbes(10); // Same probes as incoming
|
|
842
1414
|
}
|
|
843
1415
|
if ('setKeepAliveInterval' in targetSocket) {
|
|
844
|
-
|
|
1416
|
+
// Use a shorter interval on outgoing for more reliable detection
|
|
1417
|
+
(targetSocket as any).setKeepAliveInterval(800); // Slightly faster than incoming
|
|
845
1418
|
}
|
|
1419
|
+
|
|
1420
|
+
console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on outgoing connection`);
|
|
846
1421
|
} catch (err) {
|
|
847
1422
|
// Ignore errors - these are optional enhancements
|
|
848
1423
|
if (this.settings.enableDetailedLogging) {
|
|
@@ -852,6 +1427,43 @@ export class PortProxy {
|
|
|
852
1427
|
}
|
|
853
1428
|
}
|
|
854
1429
|
}
|
|
1430
|
+
|
|
1431
|
+
// Special handling for TLS keep-alive - we want to be more aggressive
|
|
1432
|
+
// with keeping the outgoing connection alive in TLS mode
|
|
1433
|
+
if (record.isTLS) {
|
|
1434
|
+
// Set a timer to periodically send empty data to keep connection alive
|
|
1435
|
+
// This is in addition to TCP keep-alive, works at application layer
|
|
1436
|
+
const keepAliveTimer = setInterval(() => {
|
|
1437
|
+
if (targetSocket && !targetSocket.destroyed && record && !record.connectionClosed) {
|
|
1438
|
+
try {
|
|
1439
|
+
// Send 0-byte packet as application-level keep-alive
|
|
1440
|
+
targetSocket.write(Buffer.alloc(0));
|
|
1441
|
+
|
|
1442
|
+
if (this.settings.enableDetailedLogging) {
|
|
1443
|
+
console.log(`[${connectionId}] Sent application-level keep-alive to outgoing TLS connection`);
|
|
1444
|
+
}
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
// If we can't write, the connection is probably already dead
|
|
1447
|
+
if (this.settings.enableDetailedLogging) {
|
|
1448
|
+
console.log(`[${connectionId}] Error sending application-level keep-alive: ${err}`);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Stop the timer if we hit an error
|
|
1452
|
+
clearInterval(keepAliveTimer);
|
|
1453
|
+
}
|
|
1454
|
+
} else {
|
|
1455
|
+
// Clean up timer if connection is gone
|
|
1456
|
+
clearInterval(keepAliveTimer);
|
|
1457
|
+
}
|
|
1458
|
+
}, 60000); // Send keep-alive every minute
|
|
1459
|
+
|
|
1460
|
+
// Make sure interval doesn't prevent process exit
|
|
1461
|
+
if (keepAliveTimer.unref) {
|
|
1462
|
+
keepAliveTimer.unref();
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
console.log(`[${connectionId}] Application-level keep-alive configured for TLS outgoing connection`);
|
|
1466
|
+
}
|
|
855
1467
|
}
|
|
856
1468
|
|
|
857
1469
|
// Setup specific error handler for connection phase with enhanced retries
|
|
@@ -1040,15 +1652,37 @@ export class PortProxy {
|
|
|
1040
1652
|
|
|
1041
1653
|
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
|
|
1042
1654
|
if (serverName && record.isTLS) {
|
|
1043
|
-
//
|
|
1655
|
+
// Create a flag to prevent double-processing of the same handshake packet
|
|
1656
|
+
let processingRenegotiation = false;
|
|
1657
|
+
|
|
1658
|
+
// This listener handles TLS renegotiation detection on the incoming socket
|
|
1044
1659
|
socket.on('data', (renegChunk) => {
|
|
1045
|
-
|
|
1660
|
+
// Only check for content type 22 (handshake) and not already processing
|
|
1661
|
+
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22 && !processingRenegotiation) {
|
|
1662
|
+
processingRenegotiation = true;
|
|
1663
|
+
|
|
1046
1664
|
// Always update activity timestamp for any handshake packet
|
|
1047
1665
|
this.updateActivity(record);
|
|
1048
1666
|
|
|
1049
1667
|
try {
|
|
1668
|
+
// Enhanced logging for renegotiation
|
|
1669
|
+
console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
|
|
1670
|
+
|
|
1050
1671
|
// Extract all TLS information including session resumption data
|
|
1051
1672
|
const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1673
|
+
|
|
1674
|
+
// Log details about the handshake packet
|
|
1675
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
1676
|
+
console.log(`[${connectionId}] Handshake SNI extraction results:`, {
|
|
1677
|
+
isResumption: sniInfo?.isResumption || false,
|
|
1678
|
+
serverName: sniInfo?.serverName || 'none',
|
|
1679
|
+
resumedDomain: sniInfo?.resumedDomain || 'none',
|
|
1680
|
+
recordsExamined: sniInfo?.recordsExamined || 0,
|
|
1681
|
+
multipleRecords: sniInfo?.multipleRecords || false,
|
|
1682
|
+
partialExtract: sniInfo?.partialExtract || false
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1052
1686
|
let newSNI = sniInfo?.serverName;
|
|
1053
1687
|
|
|
1054
1688
|
// Handle session resumption - if we recognize the session ID, we know what domain it belongs to
|
|
@@ -1057,10 +1691,17 @@ export class PortProxy {
|
|
|
1057
1691
|
newSNI = sniInfo.resumedDomain;
|
|
1058
1692
|
}
|
|
1059
1693
|
|
|
1060
|
-
// IMPORTANT: If we can't extract an SNI from renegotiation, we
|
|
1694
|
+
// IMPORTANT: If we can't extract an SNI from renegotiation, but we detected a TLS handshake,
|
|
1695
|
+
// we still need to make sure it's properly forwarded to maintain the TLS state
|
|
1061
1696
|
if (newSNI === undefined) {
|
|
1062
|
-
console.log(`[${connectionId}] Rehandshake detected without SNI,
|
|
1063
|
-
|
|
1697
|
+
console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
|
|
1698
|
+
|
|
1699
|
+
// Set a temporary timeout to reset the processing flag
|
|
1700
|
+
setTimeout(() => {
|
|
1701
|
+
processingRenegotiation = false;
|
|
1702
|
+
}, 500);
|
|
1703
|
+
|
|
1704
|
+
return; // Let the piping handle the forwarding
|
|
1064
1705
|
}
|
|
1065
1706
|
|
|
1066
1707
|
// Check if the SNI has changed
|
|
@@ -1103,15 +1744,34 @@ export class PortProxy {
|
|
|
1103
1744
|
} else {
|
|
1104
1745
|
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
|
|
1105
1746
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
1747
|
+
return;
|
|
1106
1748
|
}
|
|
1107
1749
|
} else {
|
|
1108
1750
|
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
|
|
1109
1751
|
}
|
|
1110
1752
|
} catch (err) {
|
|
1111
1753
|
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
|
|
1754
|
+
} finally {
|
|
1755
|
+
// Reset the processing flag after a small delay to prevent double-processing
|
|
1756
|
+
// of packets that may be part of the same handshake
|
|
1757
|
+
setTimeout(() => {
|
|
1758
|
+
processingRenegotiation = false;
|
|
1759
|
+
}, 500);
|
|
1112
1760
|
}
|
|
1113
1761
|
}
|
|
1114
1762
|
});
|
|
1763
|
+
|
|
1764
|
+
// Set up a listener on the outgoing socket to detect issues with renegotiation
|
|
1765
|
+
// This helps catch cases where the outgoing connection has closed but the incoming is still active
|
|
1766
|
+
targetSocket.on('error', (err) => {
|
|
1767
|
+
// If we get an error during what might be a renegotiation, log it specially
|
|
1768
|
+
if (processingRenegotiation) {
|
|
1769
|
+
console.log(`[${connectionId}] ERROR: Outgoing socket error during TLS renegotiation: ${err.message}`);
|
|
1770
|
+
// Force immediate cleanup to prevent hanging connections
|
|
1771
|
+
this.initiateCleanupOnce(record, 'renegotiation_error');
|
|
1772
|
+
}
|
|
1773
|
+
// The normal error handler will be called for other errors
|
|
1774
|
+
});
|
|
1115
1775
|
}
|
|
1116
1776
|
|
|
1117
1777
|
// Now set up piping for future data and resume the socket
|
|
@@ -1149,15 +1809,37 @@ export class PortProxy {
|
|
|
1149
1809
|
} else {
|
|
1150
1810
|
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
|
|
1151
1811
|
if (serverName && record.isTLS) {
|
|
1152
|
-
//
|
|
1812
|
+
// Create a flag to prevent double-processing of the same handshake packet
|
|
1813
|
+
let processingRenegotiation = false;
|
|
1814
|
+
|
|
1815
|
+
// This listener handles TLS renegotiation detection on the incoming socket
|
|
1153
1816
|
socket.on('data', (renegChunk) => {
|
|
1154
|
-
|
|
1817
|
+
// Only check for content type 22 (handshake) and not already processing
|
|
1818
|
+
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22 && !processingRenegotiation) {
|
|
1819
|
+
processingRenegotiation = true;
|
|
1820
|
+
|
|
1155
1821
|
// Always update activity timestamp for any handshake packet
|
|
1156
1822
|
this.updateActivity(record);
|
|
1157
1823
|
|
|
1158
1824
|
try {
|
|
1825
|
+
// Enhanced logging for renegotiation
|
|
1826
|
+
console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
|
|
1827
|
+
|
|
1159
1828
|
// Extract all TLS information including session resumption data
|
|
1160
1829
|
const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1830
|
+
|
|
1831
|
+
// Log details about the handshake packet
|
|
1832
|
+
if (this.settings.enableTlsDebugLogging) {
|
|
1833
|
+
console.log(`[${connectionId}] Handshake SNI extraction results:`, {
|
|
1834
|
+
isResumption: sniInfo?.isResumption || false,
|
|
1835
|
+
serverName: sniInfo?.serverName || 'none',
|
|
1836
|
+
resumedDomain: sniInfo?.resumedDomain || 'none',
|
|
1837
|
+
recordsExamined: sniInfo?.recordsExamined || 0,
|
|
1838
|
+
multipleRecords: sniInfo?.multipleRecords || false,
|
|
1839
|
+
partialExtract: sniInfo?.partialExtract || false
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1161
1843
|
let newSNI = sniInfo?.serverName;
|
|
1162
1844
|
|
|
1163
1845
|
// Handle session resumption - if we recognize the session ID, we know what domain it belongs to
|
|
@@ -1166,10 +1848,17 @@ export class PortProxy {
|
|
|
1166
1848
|
newSNI = sniInfo.resumedDomain;
|
|
1167
1849
|
}
|
|
1168
1850
|
|
|
1169
|
-
// IMPORTANT: If we can't extract an SNI from renegotiation, we
|
|
1851
|
+
// IMPORTANT: If we can't extract an SNI from renegotiation, but we detected a TLS handshake,
|
|
1852
|
+
// we still need to make sure it's properly forwarded to maintain the TLS state
|
|
1170
1853
|
if (newSNI === undefined) {
|
|
1171
|
-
console.log(`[${connectionId}] Rehandshake detected without SNI,
|
|
1172
|
-
|
|
1854
|
+
console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
|
|
1855
|
+
|
|
1856
|
+
// Set a temporary timeout to reset the processing flag
|
|
1857
|
+
setTimeout(() => {
|
|
1858
|
+
processingRenegotiation = false;
|
|
1859
|
+
}, 500);
|
|
1860
|
+
|
|
1861
|
+
return; // Let the piping handle the forwarding
|
|
1173
1862
|
}
|
|
1174
1863
|
|
|
1175
1864
|
// Check if the SNI has changed
|
|
@@ -1243,15 +1932,45 @@ export class PortProxy {
|
|
|
1243
1932
|
} else {
|
|
1244
1933
|
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
|
|
1245
1934
|
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
1935
|
+
return;
|
|
1246
1936
|
}
|
|
1247
1937
|
} else {
|
|
1248
1938
|
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
|
|
1249
1939
|
}
|
|
1250
1940
|
} catch (err) {
|
|
1251
1941
|
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
|
|
1942
|
+
} finally {
|
|
1943
|
+
// Reset the processing flag after a small delay to prevent double-processing
|
|
1944
|
+
// of packets that may be part of the same handshake
|
|
1945
|
+
setTimeout(() => {
|
|
1946
|
+
processingRenegotiation = false;
|
|
1947
|
+
}, 500);
|
|
1252
1948
|
}
|
|
1253
1949
|
}
|
|
1254
1950
|
});
|
|
1951
|
+
|
|
1952
|
+
// Set up a listener on the outgoing socket to detect issues with renegotiation
|
|
1953
|
+
// This helps catch cases where the outgoing connection has closed but the incoming is still active
|
|
1954
|
+
targetSocket.on('error', (err) => {
|
|
1955
|
+
// If we get an error during what might be a renegotiation, log it specially
|
|
1956
|
+
if (processingRenegotiation) {
|
|
1957
|
+
console.log(`[${connectionId}] ERROR: Outgoing socket error during TLS renegotiation: ${err.message}`);
|
|
1958
|
+
// Force immediate cleanup to prevent hanging connections
|
|
1959
|
+
this.initiateCleanupOnce(record, 'renegotiation_error');
|
|
1960
|
+
}
|
|
1961
|
+
// The normal error handler will be called for other errors
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
// Also monitor targetSocket for connection issues during client handshakes
|
|
1965
|
+
targetSocket.on('close', () => {
|
|
1966
|
+
// If the outgoing socket closes during renegotiation, it's a critical issue
|
|
1967
|
+
if (processingRenegotiation) {
|
|
1968
|
+
console.log(`[${connectionId}] CRITICAL: Outgoing socket closed during TLS renegotiation!`);
|
|
1969
|
+
console.log(`[${connectionId}] This likely explains cert mismatch errors in the browser.`);
|
|
1970
|
+
// Force immediate cleanup on the client side
|
|
1971
|
+
this.initiateCleanupOnce(record, 'target_closed_during_renegotiation');
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1255
1974
|
}
|
|
1256
1975
|
|
|
1257
1976
|
// Now set up piping
|
|
@@ -1461,7 +2180,8 @@ export class PortProxy {
|
|
|
1461
2180
|
}
|
|
1462
2181
|
|
|
1463
2182
|
/**
|
|
1464
|
-
* Update connection activity timestamp with sleep detection
|
|
2183
|
+
* Update connection activity timestamp with enhanced sleep detection
|
|
2184
|
+
* Improved for chained proxy scenarios and more aggressive handling of stale connections
|
|
1465
2185
|
*/
|
|
1466
2186
|
private updateActivity(record: IConnectionRecord): void {
|
|
1467
2187
|
// Get the current time
|
|
@@ -1471,62 +2191,108 @@ export class PortProxy {
|
|
|
1471
2191
|
if (record.lastActivity > 0) {
|
|
1472
2192
|
const timeDiff = now - record.lastActivity;
|
|
1473
2193
|
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
2194
|
+
// Enhanced sleep detection with graduated thresholds - much more relaxed
|
|
2195
|
+
// Using chain detection from settings instead of recalculating
|
|
2196
|
+
const isChainedProxy = this.settings.isChainedProxy || false;
|
|
2197
|
+
const minuteInMs = 60 * 1000;
|
|
2198
|
+
const hourInMs = 60 * minuteInMs;
|
|
2199
|
+
|
|
2200
|
+
// Significantly relaxed thresholds for better stability
|
|
2201
|
+
const shortInactivityThreshold = 30 * minuteInMs; // 30 minutes
|
|
2202
|
+
const mediumInactivityThreshold = 2 * hourInMs; // 2 hours
|
|
2203
|
+
const longInactivityThreshold = 8 * hourInMs; // 8 hours
|
|
2204
|
+
|
|
2205
|
+
// Short inactivity (10-15 mins) - Might be temporary network issue or short sleep
|
|
2206
|
+
if (timeDiff > shortInactivityThreshold) {
|
|
2207
|
+
if (record.isTLS && !record.possibleSystemSleep) {
|
|
2208
|
+
// Record first detection of possible sleep/inactivity
|
|
2209
|
+
record.possibleSystemSleep = true;
|
|
2210
|
+
record.lastSleepDetection = now;
|
|
2211
|
+
|
|
2212
|
+
if (this.settings.enableDetailedLogging) {
|
|
1490
2213
|
console.log(
|
|
1491
|
-
`[${record.id}]
|
|
1492
|
-
|
|
2214
|
+
`[${record.id}] Detected possible short inactivity for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2215
|
+
`Monitoring for TLS connection health.`
|
|
1493
2216
|
);
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// For TLS connections, send a minimal probe to check connection health
|
|
2220
|
+
if (!record.usingNetworkProxy && record.outgoing && !record.outgoing.destroyed) {
|
|
2221
|
+
try {
|
|
2222
|
+
record.outgoing.write(Buffer.alloc(0));
|
|
2223
|
+
} catch (err) {
|
|
2224
|
+
console.log(`[${record.id}] Error sending TLS probe: ${err}`);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// Medium inactivity (20-30 mins) - Likely a sleep event or network change
|
|
2231
|
+
if (timeDiff > mediumInactivityThreshold && record.hasKeepAlive) {
|
|
2232
|
+
console.log(
|
|
2233
|
+
`[${record.id}] Detected medium inactivity period for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2234
|
+
`Taking proactive steps for connection health.`
|
|
2235
|
+
);
|
|
2236
|
+
|
|
2237
|
+
// For TLS connections, we need more aggressive handling
|
|
2238
|
+
if (record.isTLS && record.tlsHandshakeComplete) {
|
|
2239
|
+
// If in a chained proxy, we should be even more aggressive about refreshing
|
|
2240
|
+
if (isChainedProxy) {
|
|
1497
2241
|
console.log(
|
|
1498
|
-
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
1499
|
-
|
|
2242
|
+
`[${record.id}] TLS connection in chained proxy inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2243
|
+
`Closing to prevent certificate inconsistencies across chain.`
|
|
1500
2244
|
);
|
|
1501
|
-
this.
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
2245
|
+
return this.initiateCleanupOnce(record, 'chained_proxy_inactivity');
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// For TLS in single proxy, try refresh first
|
|
2249
|
+
console.log(
|
|
2250
|
+
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2251
|
+
`Attempting active refresh of TLS state.`
|
|
2252
|
+
);
|
|
2253
|
+
|
|
2254
|
+
// Attempt deep TLS state refresh with buffer flush
|
|
2255
|
+
this.performDeepTlsRefresh(record);
|
|
2256
|
+
|
|
2257
|
+
// Schedule verification check with tighter timing for chained setups
|
|
2258
|
+
const verificationTimeout = isChainedProxy ? 5 * minuteInMs : 10 * minuteInMs;
|
|
2259
|
+
const refreshCheckId = record.id;
|
|
2260
|
+
const refreshCheck = setTimeout(() => {
|
|
2261
|
+
const currentRecord = this.connectionRecords.get(refreshCheckId);
|
|
2262
|
+
if (currentRecord) {
|
|
2263
|
+
const verificationTimeDiff = Date.now() - currentRecord.lastActivity;
|
|
2264
|
+
if (verificationTimeDiff > verificationTimeout / 2) {
|
|
1508
2265
|
console.log(
|
|
1509
|
-
`[${refreshCheckId}] No activity detected after TLS refresh. ` +
|
|
1510
|
-
|
|
2266
|
+
`[${refreshCheckId}] No activity detected after TLS refresh (${plugins.prettyMs(verificationTimeDiff)}). ` +
|
|
2267
|
+
`Closing connection to ensure proper browser reconnection.`
|
|
1511
2268
|
);
|
|
1512
2269
|
this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
|
|
1513
2270
|
}
|
|
1514
|
-
}, 15 * 60 * 1000);
|
|
1515
|
-
|
|
1516
|
-
// Make sure timeout doesn't keep the process alive
|
|
1517
|
-
if (refreshCheck.unref) {
|
|
1518
|
-
refreshCheck.unref();
|
|
1519
2271
|
}
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
|
|
2272
|
+
}, verificationTimeout);
|
|
2273
|
+
|
|
2274
|
+
// Make sure timeout doesn't keep the process alive
|
|
2275
|
+
if (refreshCheck.unref) {
|
|
2276
|
+
refreshCheck.unref();
|
|
1523
2277
|
}
|
|
1524
2278
|
}
|
|
1525
2279
|
|
|
1526
|
-
//
|
|
2280
|
+
// Update sleep detection markers
|
|
1527
2281
|
record.possibleSystemSleep = true;
|
|
1528
2282
|
record.lastSleepDetection = now;
|
|
1529
2283
|
}
|
|
2284
|
+
|
|
2285
|
+
// Long inactivity (60-120 mins) - Definite sleep/suspend or major network change
|
|
2286
|
+
if (timeDiff > longInactivityThreshold) {
|
|
2287
|
+
console.log(
|
|
2288
|
+
`[${record.id}] Detected long inactivity period of ${plugins.prettyMs(timeDiff)}. ` +
|
|
2289
|
+
`Closing connection to ensure fresh certificate context.`
|
|
2290
|
+
);
|
|
2291
|
+
|
|
2292
|
+
// For long periods, we always want to force close and let browser reconnect
|
|
2293
|
+
// This ensures fresh certificates and proper TLS context across the chain
|
|
2294
|
+
return this.initiateCleanupOnce(record, 'extended_inactivity_refresh');
|
|
2295
|
+
}
|
|
1530
2296
|
}
|
|
1531
2297
|
|
|
1532
2298
|
// Update the activity timestamp
|
|
@@ -1539,9 +2305,11 @@ export class PortProxy {
|
|
|
1539
2305
|
}
|
|
1540
2306
|
|
|
1541
2307
|
/**
|
|
1542
|
-
*
|
|
2308
|
+
* Perform deep TLS state refresh after sleep detection
|
|
2309
|
+
* More aggressive than the standard refresh, specifically designed for
|
|
2310
|
+
* recovering connections after system sleep in chained proxy setups
|
|
1543
2311
|
*/
|
|
1544
|
-
private
|
|
2312
|
+
private performDeepTlsRefresh(record: IConnectionRecord): void {
|
|
1545
2313
|
// Skip if we're using a NetworkProxy as it handles its own TLS state
|
|
1546
2314
|
if (record.usingNetworkProxy) {
|
|
1547
2315
|
return;
|
|
@@ -1554,31 +2322,76 @@ export class PortProxy {
|
|
|
1554
2322
|
const connectionAge = Date.now() - record.incomingStartTime;
|
|
1555
2323
|
const hourInMs = 60 * 60 * 1000;
|
|
1556
2324
|
|
|
1557
|
-
// For
|
|
1558
|
-
//
|
|
1559
|
-
if (record.isTLS && record.hasKeepAlive && connectionAge > 8 * hourInMs) { // 8 hours instead of 45 minutes
|
|
2325
|
+
// For very long-lived connections, just close them
|
|
2326
|
+
if (connectionAge > 4 * hourInMs) { // Reduced from 8 hours to 4 hours for chained proxies
|
|
1560
2327
|
console.log(
|
|
1561
2328
|
`[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
|
|
1562
|
-
|
|
2329
|
+
`Closing to ensure proper certificate handling across proxy chain.`
|
|
1563
2330
|
);
|
|
1564
|
-
return this.initiateCleanupOnce(record, '
|
|
2331
|
+
return this.initiateCleanupOnce(record, 'certificate_age_refresh');
|
|
1565
2332
|
}
|
|
1566
2333
|
|
|
1567
|
-
//
|
|
2334
|
+
// Perform a series of actions to try to refresh the TLS state
|
|
2335
|
+
|
|
2336
|
+
// 1. Send a zero-length buffer to trigger any pending errors
|
|
1568
2337
|
record.outgoing.write(Buffer.alloc(0));
|
|
2338
|
+
|
|
2339
|
+
// 2. Check socket state
|
|
2340
|
+
if (record.outgoing.writableEnded || !record.outgoing.writable) {
|
|
2341
|
+
console.log(`[${record.id}] Socket no longer writable during refresh`);
|
|
2342
|
+
return this.initiateCleanupOnce(record, 'socket_state_error');
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
// 3. For TLS connections, try to force background renegotiation
|
|
2346
|
+
// by manipulating socket timeouts
|
|
2347
|
+
const originalTimeout = record.outgoing.timeout;
|
|
2348
|
+
record.outgoing.setTimeout(100); // Set very short timeout
|
|
2349
|
+
|
|
2350
|
+
// 4. Create a small delay to allow timeout to process
|
|
2351
|
+
setTimeout(() => {
|
|
2352
|
+
try {
|
|
2353
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
2354
|
+
// Reset timeout to original value
|
|
2355
|
+
record.outgoing.setTimeout(originalTimeout || 0);
|
|
2356
|
+
|
|
2357
|
+
// Send another probe with random data (16 bytes) that will be ignored by TLS layer
|
|
2358
|
+
// but might trigger internal state updates in the TLS implementation
|
|
2359
|
+
const probeBuffer = Buffer.alloc(16);
|
|
2360
|
+
// Fill with random data
|
|
2361
|
+
for (let i = 0; i < 16; i++) {
|
|
2362
|
+
probeBuffer[i] = Math.floor(Math.random() * 256);
|
|
2363
|
+
}
|
|
2364
|
+
record.outgoing.write(Buffer.alloc(0));
|
|
2365
|
+
|
|
2366
|
+
if (this.settings.enableDetailedLogging) {
|
|
2367
|
+
console.log(`[${record.id}] Completed deep TLS refresh sequence`);
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
} catch (innerErr) {
|
|
2371
|
+
console.log(`[${record.id}] Error during deep TLS refresh: ${innerErr}`);
|
|
2372
|
+
this.initiateCleanupOnce(record, 'deep_refresh_error');
|
|
2373
|
+
}
|
|
2374
|
+
}, 150);
|
|
1569
2375
|
|
|
1570
2376
|
if (this.settings.enableDetailedLogging) {
|
|
1571
|
-
console.log(`[${record.id}]
|
|
2377
|
+
console.log(`[${record.id}] Initiated deep TLS refresh sequence`);
|
|
1572
2378
|
}
|
|
1573
2379
|
}
|
|
1574
2380
|
} catch (err) {
|
|
1575
|
-
console.log(`[${record.id}] Error
|
|
2381
|
+
console.log(`[${record.id}] Error starting TLS state refresh: ${err}`);
|
|
1576
2382
|
|
|
1577
2383
|
// If we hit an error, it's likely the connection is already broken
|
|
1578
2384
|
// Force cleanup to ensure browser reconnects cleanly
|
|
1579
2385
|
return this.initiateCleanupOnce(record, 'tls_refresh_error');
|
|
1580
2386
|
}
|
|
1581
2387
|
}
|
|
2388
|
+
|
|
2389
|
+
/**
|
|
2390
|
+
* Legacy refresh method for backward compatibility
|
|
2391
|
+
*/
|
|
2392
|
+
private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
|
|
2393
|
+
return this.performDeepTlsRefresh(record);
|
|
2394
|
+
}
|
|
1582
2395
|
|
|
1583
2396
|
/**
|
|
1584
2397
|
* Cleans up a connection record.
|
|
@@ -1872,12 +2685,20 @@ export class PortProxy {
|
|
|
1872
2685
|
|
|
1873
2686
|
// Initialize sleep detection fields
|
|
1874
2687
|
possibleSystemSleep: false,
|
|
2688
|
+
|
|
2689
|
+
// Track keep-alive state for both sides of the connection
|
|
2690
|
+
incomingKeepAliveEnabled: false,
|
|
2691
|
+
outgoingKeepAliveEnabled: false,
|
|
1875
2692
|
};
|
|
1876
2693
|
|
|
1877
2694
|
// Apply keep-alive settings if enabled
|
|
1878
2695
|
if (this.settings.keepAlive) {
|
|
2696
|
+
// Configure incoming socket keep-alive
|
|
1879
2697
|
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
1880
2698
|
connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
|
|
2699
|
+
connectionRecord.incomingKeepAliveEnabled = true;
|
|
2700
|
+
|
|
2701
|
+
console.log(`[${connectionId}] Keep-alive enabled on incoming connection with initial delay: ${this.settings.keepAliveInitialDelay}ms`);
|
|
1881
2702
|
|
|
1882
2703
|
// Apply enhanced TCP keep-alive options if enabled
|
|
1883
2704
|
if (this.settings.enableKeepAliveProbes) {
|
|
@@ -1889,6 +2710,8 @@ export class PortProxy {
|
|
|
1889
2710
|
if ('setKeepAliveInterval' in socket) {
|
|
1890
2711
|
(socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
|
|
1891
2712
|
}
|
|
2713
|
+
|
|
2714
|
+
console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on incoming connection`);
|
|
1892
2715
|
} catch (err) {
|
|
1893
2716
|
// Ignore errors - these are optional enhancements
|
|
1894
2717
|
if (this.settings.enableDetailedLogging) {
|