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