@push.rocks/smartproxy 3.32.1 → 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 +138 -1424
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +282 -1920
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,810 +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
|
-
incomingKeepAliveEnabled?: boolean; // Whether keep-alive is enabled on incoming socket
|
|
104
|
-
outgoingKeepAliveEnabled?: boolean; // Whether keep-alive is enabled on outgoing socket
|
|
105
96
|
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
|
|
106
97
|
incomingTerminationReason?: string | null; // Reason for incoming termination
|
|
107
98
|
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
|
108
|
-
|
|
99
|
+
|
|
109
100
|
// New field for NetworkProxy tracking
|
|
110
101
|
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
|
111
102
|
networkProxyIndex?: number; // Which NetworkProxy instance is being used
|
|
112
|
-
|
|
113
|
-
// Sleep detection fields
|
|
114
|
-
possibleSystemSleep?: boolean; // Flag to indicate a possible system sleep was detected
|
|
115
|
-
lastSleepDetection?: number; // Timestamp of the last sleep detection
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Structure to track TLS session information for proper resumption handling
|
|
120
|
-
*/
|
|
121
|
-
interface ITlsSessionInfo {
|
|
122
|
-
domain: string; // The SNI domain associated with this session
|
|
123
|
-
sessionId?: Buffer; // The TLS session ID (if available)
|
|
124
|
-
ticketId?: string; // Session ticket identifier for newer TLS versions
|
|
125
|
-
ticketTimestamp: number; // When this session was recorded
|
|
126
|
-
lastAccessed?: number; // When this session was last accessed
|
|
127
|
-
accessCount?: number; // How many times this session has been used
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Configuration for TLS session cache
|
|
132
|
-
*/
|
|
133
|
-
interface ITlsSessionCacheConfig {
|
|
134
|
-
maxEntries: number; // Maximum number of entries to keep in the cache
|
|
135
|
-
expiryTime: number; // Time in ms before sessions expire (default: 24 hours)
|
|
136
|
-
cleanupInterval: number; // Interval in ms to run cleanup (default: 10 minutes)
|
|
137
|
-
enabled: boolean; // Whether session caching is enabled
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Default configuration for session cache with relaxed timeouts
|
|
141
|
-
const DEFAULT_SESSION_CACHE_CONFIG: ITlsSessionCacheConfig = {
|
|
142
|
-
maxEntries: 20000, // Default max 20,000 entries (doubled)
|
|
143
|
-
expiryTime: 7 * 24 * 60 * 60 * 1000, // 7 days default (increased from 24 hours)
|
|
144
|
-
cleanupInterval: 30 * 60 * 1000, // Clean up every 30 minutes (relaxed from 10 minutes)
|
|
145
|
-
enabled: true // Enabled by default
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// Enhanced TLS session cache with size limits and better performance
|
|
149
|
-
class TlsSessionCache {
|
|
150
|
-
private cache = new Map<string, ITlsSessionInfo>();
|
|
151
|
-
private config: ITlsSessionCacheConfig;
|
|
152
|
-
private cleanupTimer: NodeJS.Timeout | null = null;
|
|
153
|
-
private lastCleanupTime: number = 0;
|
|
154
|
-
private cacheStats = {
|
|
155
|
-
hits: 0,
|
|
156
|
-
misses: 0,
|
|
157
|
-
expirations: 0,
|
|
158
|
-
evictions: 0,
|
|
159
|
-
total: 0
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
constructor(config?: Partial<ITlsSessionCacheConfig>) {
|
|
163
|
-
this.config = { ...DEFAULT_SESSION_CACHE_CONFIG, ...config };
|
|
164
|
-
this.startCleanupTimer();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Get a session from the cache
|
|
169
|
-
*/
|
|
170
|
-
public get(key: string): ITlsSessionInfo | undefined {
|
|
171
|
-
// Skip if cache is disabled
|
|
172
|
-
if (!this.config.enabled) return undefined;
|
|
173
|
-
|
|
174
|
-
const entry = this.cache.get(key);
|
|
175
|
-
|
|
176
|
-
if (entry) {
|
|
177
|
-
// Update access information
|
|
178
|
-
entry.lastAccessed = Date.now();
|
|
179
|
-
entry.accessCount = (entry.accessCount || 0) + 1;
|
|
180
|
-
this.cache.set(key, entry);
|
|
181
|
-
this.cacheStats.hits++;
|
|
182
|
-
return entry;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
this.cacheStats.misses++;
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Check if the cache has a key
|
|
191
|
-
*/
|
|
192
|
-
public has(key: string): boolean {
|
|
193
|
-
// Skip if cache is disabled
|
|
194
|
-
if (!this.config.enabled) return false;
|
|
195
|
-
|
|
196
|
-
const exists = this.cache.has(key);
|
|
197
|
-
if (exists) {
|
|
198
|
-
const entry = this.cache.get(key)!;
|
|
199
|
-
|
|
200
|
-
// Check if entry has expired
|
|
201
|
-
if (Date.now() - entry.ticketTimestamp > this.config.expiryTime) {
|
|
202
|
-
this.cache.delete(key);
|
|
203
|
-
this.cacheStats.expirations++;
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Update last accessed time
|
|
208
|
-
entry.lastAccessed = Date.now();
|
|
209
|
-
this.cache.set(key, entry);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return exists;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Set a session in the cache
|
|
217
|
-
*/
|
|
218
|
-
public set(key: string, value: ITlsSessionInfo): void {
|
|
219
|
-
// Skip if cache is disabled
|
|
220
|
-
if (!this.config.enabled) return;
|
|
221
|
-
|
|
222
|
-
// Ensure timestamps are set
|
|
223
|
-
const entry = {
|
|
224
|
-
...value,
|
|
225
|
-
lastAccessed: Date.now(),
|
|
226
|
-
accessCount: 0
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
// Check if we need to evict entries
|
|
230
|
-
if (!this.cache.has(key) && this.cache.size >= this.config.maxEntries) {
|
|
231
|
-
this.evictOldest();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
this.cache.set(key, entry);
|
|
235
|
-
this.cacheStats.total = this.cache.size;
|
|
236
|
-
|
|
237
|
-
// Run cleanup if it's been a while
|
|
238
|
-
const timeSinceCleanup = Date.now() - this.lastCleanupTime;
|
|
239
|
-
if (timeSinceCleanup > this.config.cleanupInterval * 2) {
|
|
240
|
-
this.cleanup();
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Delete a session from the cache
|
|
246
|
-
*/
|
|
247
|
-
public delete(key: string): boolean {
|
|
248
|
-
return this.cache.delete(key);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Clear the entire cache
|
|
253
|
-
*/
|
|
254
|
-
public clear(): void {
|
|
255
|
-
this.cache.clear();
|
|
256
|
-
this.cacheStats.total = 0;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Get cache statistics
|
|
261
|
-
*/
|
|
262
|
-
public getStats(): any {
|
|
263
|
-
return {
|
|
264
|
-
...this.cacheStats,
|
|
265
|
-
size: this.cache.size,
|
|
266
|
-
enabled: this.config.enabled,
|
|
267
|
-
maxEntries: this.config.maxEntries,
|
|
268
|
-
expiryTimeHours: this.config.expiryTime / (60 * 60 * 1000)
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Update cache configuration
|
|
274
|
-
*/
|
|
275
|
-
public updateConfig(config: Partial<ITlsSessionCacheConfig>): void {
|
|
276
|
-
this.config = { ...this.config, ...config };
|
|
277
|
-
|
|
278
|
-
// Restart the cleanup timer with new interval
|
|
279
|
-
this.startCleanupTimer();
|
|
280
|
-
|
|
281
|
-
// Run immediate cleanup if max entries was reduced
|
|
282
|
-
if (config.maxEntries && this.cache.size > config.maxEntries) {
|
|
283
|
-
while (this.cache.size > config.maxEntries) {
|
|
284
|
-
this.evictOldest();
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Start the cleanup timer
|
|
291
|
-
*/
|
|
292
|
-
private startCleanupTimer(): void {
|
|
293
|
-
if (this.cleanupTimer) {
|
|
294
|
-
clearInterval(this.cleanupTimer);
|
|
295
|
-
this.cleanupTimer = null;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (!this.config.enabled) return;
|
|
299
|
-
|
|
300
|
-
this.cleanupTimer = setInterval(() => {
|
|
301
|
-
this.cleanup();
|
|
302
|
-
}, this.config.cleanupInterval);
|
|
303
|
-
|
|
304
|
-
// Make sure the interval doesn't keep the process alive
|
|
305
|
-
if (this.cleanupTimer.unref) {
|
|
306
|
-
this.cleanupTimer.unref();
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Clean up expired entries
|
|
312
|
-
*/
|
|
313
|
-
private cleanup(): void {
|
|
314
|
-
this.lastCleanupTime = Date.now();
|
|
315
|
-
|
|
316
|
-
const now = Date.now();
|
|
317
|
-
let expiredCount = 0;
|
|
318
|
-
|
|
319
|
-
for (const [key, info] of this.cache.entries()) {
|
|
320
|
-
if (now - info.ticketTimestamp > this.config.expiryTime) {
|
|
321
|
-
this.cache.delete(key);
|
|
322
|
-
expiredCount++;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
if (expiredCount > 0) {
|
|
327
|
-
this.cacheStats.expirations += expiredCount;
|
|
328
|
-
this.cacheStats.total = this.cache.size;
|
|
329
|
-
console.log(`TLS Session Cache: Cleaned up ${expiredCount} expired entries. ${this.cache.size} entries remaining.`);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Evict the oldest entries when cache is full
|
|
335
|
-
*/
|
|
336
|
-
private evictOldest(): void {
|
|
337
|
-
if (this.cache.size === 0) return;
|
|
338
|
-
|
|
339
|
-
let oldestKey: string | null = null;
|
|
340
|
-
let oldestTime = Date.now();
|
|
341
|
-
|
|
342
|
-
// Strategy: Find least recently accessed entry
|
|
343
|
-
for (const [key, info] of this.cache.entries()) {
|
|
344
|
-
const lastAccess = info.lastAccessed || info.ticketTimestamp;
|
|
345
|
-
if (lastAccess < oldestTime) {
|
|
346
|
-
oldestTime = lastAccess;
|
|
347
|
-
oldestKey = key;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
if (oldestKey) {
|
|
352
|
-
this.cache.delete(oldestKey);
|
|
353
|
-
this.cacheStats.evictions++;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Stop cleanup timer (used during shutdown)
|
|
359
|
-
*/
|
|
360
|
-
public stop(): void {
|
|
361
|
-
if (this.cleanupTimer) {
|
|
362
|
-
clearInterval(this.cleanupTimer);
|
|
363
|
-
this.cleanupTimer = null;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Create the global session cache
|
|
369
|
-
const tlsSessionCache = new TlsSessionCache();
|
|
370
|
-
|
|
371
|
-
// Legacy function for backward compatibility
|
|
372
|
-
function stopSessionCleanupTimer() {
|
|
373
|
-
tlsSessionCache.stop();
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* Return type for the extractSNIInfo function
|
|
378
|
-
*/
|
|
379
|
-
interface ISNIExtractResult {
|
|
380
|
-
serverName?: string; // The extracted SNI hostname
|
|
381
|
-
sessionId?: Buffer; // The TLS session ID if present
|
|
382
|
-
sessionIdKey?: string; // The hex string representation of session ID
|
|
383
|
-
sessionTicketId?: string; // Session ticket identifier for TLS 1.3+ resumption
|
|
384
|
-
hasSessionTicket?: boolean; // Whether a session ticket extension was found
|
|
385
|
-
isResumption: boolean; // Whether this appears to be a session resumption
|
|
386
|
-
resumedDomain?: string; // The domain associated with the session if resuming
|
|
387
|
-
partialExtract?: boolean; // Whether this was only a partial extraction (more data needed)
|
|
388
|
-
recordsExamined?: number; // Number of TLS records examined in the buffer
|
|
389
|
-
multipleRecords?: boolean; // Whether multiple TLS records were found in the buffer
|
|
390
103
|
}
|
|
391
104
|
|
|
392
105
|
/**
|
|
393
106
|
* Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
|
|
394
107
|
* Enhanced for robustness and detailed logging.
|
|
395
|
-
* Also extracts and tracks TLS Session IDs for session resumption handling.
|
|
396
|
-
*
|
|
397
|
-
* Improved to handle:
|
|
398
|
-
* - Multiple TLS records in a single buffer
|
|
399
|
-
* - Fragmented TLS handshakes across multiple records
|
|
400
|
-
* - Partial TLS records that may continue in future chunks
|
|
401
|
-
*
|
|
402
108
|
* @param buffer - Buffer containing the TLS ClientHello.
|
|
403
109
|
* @param enableLogging - Whether to enable detailed logging.
|
|
404
|
-
* @returns
|
|
110
|
+
* @returns The server name if found, otherwise undefined.
|
|
405
111
|
*/
|
|
406
|
-
function
|
|
112
|
+
function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
|
|
407
113
|
try {
|
|
408
114
|
// Check if buffer is too small for TLS
|
|
409
115
|
if (buffer.length < 5) {
|
|
410
116
|
if (enableLogging) console.log('Buffer too small for TLS header');
|
|
411
|
-
return
|
|
412
|
-
isResumption: false,
|
|
413
|
-
partialExtract: true // Indicating we need more data
|
|
414
|
-
};
|
|
117
|
+
return undefined;
|
|
415
118
|
}
|
|
416
119
|
|
|
417
|
-
// Check
|
|
120
|
+
// Check record type (has to be handshake - 22)
|
|
418
121
|
const recordType = buffer.readUInt8(0);
|
|
419
122
|
if (recordType !== 22) {
|
|
420
123
|
if (enableLogging) console.log(`Not a TLS handshake. Record type: ${recordType}`);
|
|
421
124
|
return undefined;
|
|
422
125
|
}
|
|
423
126
|
|
|
424
|
-
//
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
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}`);
|
|
429
131
|
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (currentRecordType !== 22) {
|
|
439
|
-
if (enableLogging) console.log(`Skipping non-handshake record at position ${currentPosition}, type: ${currentRecordType}`);
|
|
440
|
-
|
|
441
|
-
// Move to next potential record
|
|
442
|
-
if (currentPosition + 5 <= buffer.length) {
|
|
443
|
-
// Need at least 5 bytes to determine next record length
|
|
444
|
-
const nextRecordLength = buffer.readUInt16BE(currentPosition + 3);
|
|
445
|
-
currentPosition += 5 + nextRecordLength;
|
|
446
|
-
multipleRecords = true;
|
|
447
|
-
continue;
|
|
448
|
-
} else {
|
|
449
|
-
// Not enough data to determine next record
|
|
450
|
-
break;
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Check TLS version
|
|
455
|
-
const majorVersion = buffer.readUInt8(currentPosition + 1);
|
|
456
|
-
const minorVersion = buffer.readUInt8(currentPosition + 2);
|
|
457
|
-
if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion} at position ${currentPosition}`);
|
|
458
|
-
|
|
459
|
-
// Get record length
|
|
460
|
-
const recordLength = buffer.readUInt16BE(currentPosition + 3);
|
|
461
|
-
|
|
462
|
-
// Check if we have the complete record
|
|
463
|
-
if (currentPosition + 5 + recordLength > buffer.length) {
|
|
464
|
-
if (enableLogging) {
|
|
465
|
-
console.log(`Incomplete TLS record at position ${currentPosition}. Expected: ${currentPosition + 5 + recordLength}, Got: ${buffer.length}`);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Return partial info and signal that more data is needed
|
|
469
|
-
return {
|
|
470
|
-
isResumption: false,
|
|
471
|
-
partialExtract: true,
|
|
472
|
-
recordsExamined,
|
|
473
|
-
multipleRecords
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Process this record - extract handshake information
|
|
478
|
-
const recordResult = extractSNIFromRecord(
|
|
479
|
-
buffer.slice(currentPosition, currentPosition + 5 + recordLength),
|
|
480
|
-
enableLogging
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
// If we found SNI or session info in this record, store it
|
|
484
|
-
if (recordResult && (recordResult.serverName || recordResult.isResumption)) {
|
|
485
|
-
result = recordResult;
|
|
486
|
-
result.recordsExamined = recordsExamined;
|
|
487
|
-
result.multipleRecords = multipleRecords;
|
|
488
|
-
|
|
489
|
-
// Once we've found SNI or session resumption info, we can stop processing
|
|
490
|
-
// But we'll still set the multipleRecords flag to indicate more records exist
|
|
491
|
-
if (currentPosition + 5 + recordLength < buffer.length) {
|
|
492
|
-
result.multipleRecords = true;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
break;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Move to the next record
|
|
499
|
-
currentPosition += 5 + recordLength;
|
|
500
|
-
|
|
501
|
-
// Set the flag if we've processed multiple records
|
|
502
|
-
if (currentPosition < buffer.length) {
|
|
503
|
-
multipleRecords = true;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// If we processed records but didn't find SNI or session info
|
|
508
|
-
if (recordsExamined > 0 && !result) {
|
|
509
|
-
return {
|
|
510
|
-
isResumption: false,
|
|
511
|
-
recordsExamined,
|
|
512
|
-
multipleRecords
|
|
513
|
-
};
|
|
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;
|
|
514
140
|
}
|
|
515
|
-
|
|
516
|
-
return result;
|
|
517
|
-
} catch (err) {
|
|
518
|
-
console.log(`Error extracting SNI: ${err}`);
|
|
519
|
-
return undefined;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
141
|
|
|
523
|
-
/**
|
|
524
|
-
* Extracts SNI information from a single TLS record
|
|
525
|
-
* This helper function processes a single complete TLS record
|
|
526
|
-
*/
|
|
527
|
-
function extractSNIFromRecord(recordBuffer: Buffer, enableLogging: boolean = false): ISNIExtractResult | undefined {
|
|
528
|
-
try {
|
|
529
|
-
// Skip the 5-byte TLS record header
|
|
530
142
|
let offset = 5;
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const handshakeType = recordBuffer.readUInt8(offset);
|
|
534
|
-
if (handshakeType !== 1) { // 1 = ClientHello
|
|
143
|
+
const handshakeType = buffer.readUInt8(offset);
|
|
144
|
+
if (handshakeType !== 1) {
|
|
535
145
|
if (enableLogging) console.log(`Not a ClientHello. Handshake type: ${handshakeType}`);
|
|
536
146
|
return undefined;
|
|
537
147
|
}
|
|
538
|
-
|
|
539
|
-
// Skip
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
// Check if we have at least 38 more bytes for protocol version and random
|
|
543
|
-
if (offset + 38 > recordBuffer.length) {
|
|
544
|
-
if (enableLogging) console.log('Buffer too small for handshake header');
|
|
545
|
-
return undefined;
|
|
546
|
-
}
|
|
547
|
-
|
|
148
|
+
|
|
149
|
+
offset += 4; // Skip handshake header (type + length)
|
|
150
|
+
|
|
548
151
|
// Client version
|
|
549
|
-
const clientMajorVersion =
|
|
550
|
-
const clientMinorVersion =
|
|
152
|
+
const clientMajorVersion = buffer.readUInt8(offset);
|
|
153
|
+
const clientMinorVersion = buffer.readUInt8(offset + 1);
|
|
551
154
|
if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
|
|
552
|
-
|
|
553
|
-
// Skip version and random
|
|
554
|
-
|
|
555
|
-
|
|
155
|
+
|
|
156
|
+
offset += 2 + 32; // Skip client version and random
|
|
157
|
+
|
|
556
158
|
// Session ID
|
|
557
|
-
|
|
558
|
-
if (enableLogging) console.log('Buffer too small for session ID length');
|
|
559
|
-
return undefined;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Extract Session ID for session resumption tracking
|
|
563
|
-
const sessionIDLength = recordBuffer.readUInt8(offset);
|
|
159
|
+
const sessionIDLength = buffer.readUInt8(offset);
|
|
564
160
|
if (enableLogging) console.log(`Session ID Length: ${sessionIDLength}`);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
let sessionId: Buffer | undefined;
|
|
568
|
-
let sessionIdKey: string | undefined;
|
|
569
|
-
let isResumption = false;
|
|
570
|
-
let resumedDomain: string | undefined;
|
|
571
|
-
|
|
572
|
-
if (sessionIDLength > 0) {
|
|
573
|
-
if (offset + 1 + sessionIDLength > recordBuffer.length) {
|
|
574
|
-
if (enableLogging) console.log('Buffer too small for session ID data');
|
|
575
|
-
return undefined;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
sessionId = Buffer.from(recordBuffer.slice(offset + 1, offset + 1 + sessionIDLength));
|
|
579
|
-
|
|
580
|
-
// Convert sessionId to a string key for our cache
|
|
581
|
-
sessionIdKey = sessionId.toString('hex');
|
|
582
|
-
|
|
583
|
-
if (enableLogging) {
|
|
584
|
-
console.log(`Session ID: ${sessionIdKey}`);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Check if this is a session resumption attempt
|
|
588
|
-
if (tlsSessionCache.has(sessionIdKey)) {
|
|
589
|
-
const cachedInfo = tlsSessionCache.get(sessionIdKey)!;
|
|
590
|
-
resumedDomain = cachedInfo.domain;
|
|
591
|
-
isResumption = true;
|
|
592
|
-
|
|
593
|
-
if (enableLogging) {
|
|
594
|
-
console.log(`TLS Session Resumption detected for domain: ${resumedDomain}`);
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
offset += 1 + sessionIDLength; // Skip session ID length and data
|
|
600
|
-
|
|
161
|
+
offset += 1 + sessionIDLength; // Skip session ID
|
|
162
|
+
|
|
601
163
|
// Cipher suites
|
|
602
|
-
if (offset + 2 >
|
|
164
|
+
if (offset + 2 > buffer.length) {
|
|
603
165
|
if (enableLogging) console.log('Buffer too small for cipher suites length');
|
|
604
166
|
return undefined;
|
|
605
167
|
}
|
|
606
|
-
|
|
607
|
-
const cipherSuitesLength = recordBuffer.readUInt16BE(offset);
|
|
168
|
+
const cipherSuitesLength = buffer.readUInt16BE(offset);
|
|
608
169
|
if (enableLogging) console.log(`Cipher Suites Length: ${cipherSuitesLength}`);
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
if (enableLogging) console.log('Buffer too small for cipher suites data');
|
|
612
|
-
return undefined;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
offset += 2 + cipherSuitesLength; // Skip cipher suites length and data
|
|
616
|
-
|
|
170
|
+
offset += 2 + cipherSuitesLength; // Skip cipher suites
|
|
171
|
+
|
|
617
172
|
// Compression methods
|
|
618
|
-
if (offset + 1 >
|
|
173
|
+
if (offset + 1 > buffer.length) {
|
|
619
174
|
if (enableLogging) console.log('Buffer too small for compression methods length');
|
|
620
175
|
return undefined;
|
|
621
176
|
}
|
|
622
|
-
|
|
623
|
-
const compressionMethodsLength = recordBuffer.readUInt8(offset);
|
|
177
|
+
const compressionMethodsLength = buffer.readUInt8(offset);
|
|
624
178
|
if (enableLogging) console.log(`Compression Methods Length: ${compressionMethodsLength}`);
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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');
|
|
628
184
|
return undefined;
|
|
629
185
|
}
|
|
630
|
-
|
|
631
|
-
offset += 1 + compressionMethodsLength; // Skip compression methods length and data
|
|
632
|
-
|
|
633
|
-
// Check if we have extensions data
|
|
634
|
-
if (offset + 2 > recordBuffer.length) {
|
|
635
|
-
if (enableLogging) console.log('No extensions data found - end of ClientHello');
|
|
636
|
-
|
|
637
|
-
// Even without SNI, we might be dealing with a session resumption
|
|
638
|
-
if (isResumption && resumedDomain) {
|
|
639
|
-
return {
|
|
640
|
-
serverName: resumedDomain, // Use the domain from previous session
|
|
641
|
-
sessionId,
|
|
642
|
-
sessionIdKey,
|
|
643
|
-
hasSessionTicket: false,
|
|
644
|
-
isResumption: true,
|
|
645
|
-
resumedDomain
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
return {
|
|
650
|
-
isResumption,
|
|
651
|
-
sessionId,
|
|
652
|
-
sessionIdKey
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
// Extensions
|
|
657
|
-
const extensionsLength = recordBuffer.readUInt16BE(offset);
|
|
186
|
+
const extensionsLength = buffer.readUInt16BE(offset);
|
|
658
187
|
if (enableLogging) console.log(`Extensions Length: ${extensionsLength}`);
|
|
659
|
-
|
|
660
188
|
offset += 2;
|
|
661
189
|
const extensionsEnd = offset + extensionsLength;
|
|
662
|
-
|
|
663
|
-
if (extensionsEnd >
|
|
664
|
-
if (enableLogging)
|
|
665
|
-
console.log(
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (isResumption && resumedDomain) {
|
|
670
|
-
return {
|
|
671
|
-
serverName: resumedDomain, // Use the domain from previous session
|
|
672
|
-
sessionId,
|
|
673
|
-
sessionIdKey,
|
|
674
|
-
hasSessionTicket: false,
|
|
675
|
-
isResumption: true,
|
|
676
|
-
resumedDomain
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
return {
|
|
681
|
-
isResumption,
|
|
682
|
-
sessionId,
|
|
683
|
-
sessionIdKey,
|
|
684
|
-
partialExtract: true // Indicating we have incomplete extensions data
|
|
685
|
-
};
|
|
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;
|
|
686
197
|
}
|
|
687
|
-
|
|
688
|
-
// Variables to track session tickets
|
|
689
|
-
let hasSessionTicket = false;
|
|
690
|
-
let sessionTicketId: string | undefined;
|
|
691
|
-
|
|
198
|
+
|
|
692
199
|
// Parse extensions
|
|
693
200
|
while (offset + 4 <= extensionsEnd) {
|
|
694
|
-
const extensionType =
|
|
695
|
-
const extensionLength =
|
|
696
|
-
|
|
697
|
-
if (enableLogging)
|
|
201
|
+
const extensionType = buffer.readUInt16BE(offset);
|
|
202
|
+
const extensionLength = buffer.readUInt16BE(offset + 2);
|
|
203
|
+
|
|
204
|
+
if (enableLogging)
|
|
698
205
|
console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
if (offset + 4 + extensionLength > recordBuffer.length) {
|
|
702
|
-
if (enableLogging) {
|
|
703
|
-
console.log(`Extension data incomplete. Expected: ${offset + 4 + extensionLength}, Got: ${recordBuffer.length}`);
|
|
704
|
-
}
|
|
705
|
-
return {
|
|
706
|
-
isResumption,
|
|
707
|
-
sessionId,
|
|
708
|
-
sessionIdKey,
|
|
709
|
-
hasSessionTicket,
|
|
710
|
-
partialExtract: true
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
|
|
206
|
+
|
|
714
207
|
offset += 4;
|
|
715
|
-
|
|
716
|
-
// Check for Session Ticket extension (type 0x0023)
|
|
717
|
-
if (extensionType === 0x0023 && extensionLength > 0) {
|
|
718
|
-
hasSessionTicket = true;
|
|
719
|
-
|
|
720
|
-
// Extract a hash of the ticket for tracking
|
|
721
|
-
if (extensionLength > 16) { // Ensure we have enough bytes to create a meaningful ID
|
|
722
|
-
const ticketBytes = recordBuffer.slice(offset, offset + Math.min(16, extensionLength));
|
|
723
|
-
sessionTicketId = ticketBytes.toString('hex');
|
|
724
|
-
|
|
725
|
-
if (enableLogging) {
|
|
726
|
-
console.log(`Session Ticket found, ID: ${sessionTicketId}`);
|
|
727
|
-
|
|
728
|
-
// Check if this is a known session ticket
|
|
729
|
-
if (tlsSessionCache.has(`ticket:${sessionTicketId}`)) {
|
|
730
|
-
const cachedInfo = tlsSessionCache.get(`ticket:${sessionTicketId}`);
|
|
731
|
-
console.log(`TLS Session Ticket Resumption detected for domain: ${cachedInfo?.domain}`);
|
|
732
|
-
|
|
733
|
-
// Set isResumption and resumedDomain if not already set
|
|
734
|
-
if (!isResumption && !resumedDomain) {
|
|
735
|
-
isResumption = true;
|
|
736
|
-
resumedDomain = cachedInfo?.domain;
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Server Name Indication extension (type 0x0000)
|
|
208
|
+
|
|
744
209
|
if (extensionType === 0x0000) {
|
|
745
|
-
|
|
210
|
+
// SNI extension
|
|
211
|
+
if (offset + 2 > buffer.length) {
|
|
746
212
|
if (enableLogging) console.log('Buffer too small for SNI list length');
|
|
747
|
-
return
|
|
748
|
-
isResumption,
|
|
749
|
-
sessionId,
|
|
750
|
-
sessionIdKey,
|
|
751
|
-
hasSessionTicket,
|
|
752
|
-
partialExtract: true
|
|
753
|
-
};
|
|
213
|
+
return undefined;
|
|
754
214
|
}
|
|
755
|
-
|
|
756
|
-
const sniListLength =
|
|
215
|
+
|
|
216
|
+
const sniListLength = buffer.readUInt16BE(offset);
|
|
757
217
|
if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
|
|
758
|
-
|
|
759
218
|
offset += 2;
|
|
760
219
|
const sniListEnd = offset + sniListLength;
|
|
761
|
-
|
|
762
|
-
if (sniListEnd >
|
|
763
|
-
if (enableLogging)
|
|
764
|
-
console.log(
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
sessionId,
|
|
769
|
-
sessionIdKey,
|
|
770
|
-
hasSessionTicket,
|
|
771
|
-
partialExtract: true
|
|
772
|
-
};
|
|
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;
|
|
773
227
|
}
|
|
774
|
-
|
|
228
|
+
|
|
775
229
|
while (offset + 3 < sniListEnd) {
|
|
776
|
-
const nameType =
|
|
777
|
-
|
|
778
|
-
if (offset + 2 > recordBuffer.length) {
|
|
779
|
-
if (enableLogging) console.log('Buffer too small for SNI name length');
|
|
780
|
-
return {
|
|
781
|
-
isResumption,
|
|
782
|
-
sessionId,
|
|
783
|
-
sessionIdKey,
|
|
784
|
-
hasSessionTicket,
|
|
785
|
-
partialExtract: true
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const nameLen = recordBuffer.readUInt16BE(offset);
|
|
230
|
+
const nameType = buffer.readUInt8(offset++);
|
|
231
|
+
const nameLen = buffer.readUInt16BE(offset);
|
|
790
232
|
offset += 2;
|
|
791
|
-
|
|
233
|
+
|
|
792
234
|
if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
|
|
793
|
-
|
|
794
|
-
// Only process hostname entries (type 0)
|
|
235
|
+
|
|
795
236
|
if (nameType === 0) {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
partialExtract: true
|
|
806
|
-
};
|
|
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;
|
|
807
246
|
}
|
|
808
|
-
|
|
809
|
-
const serverName =
|
|
247
|
+
|
|
248
|
+
const serverName = buffer.toString('utf8', offset, offset + nameLen);
|
|
810
249
|
if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
|
|
811
|
-
|
|
812
|
-
// Store the session ID to domain mapping for future resumptions
|
|
813
|
-
if (sessionIdKey && sessionId && serverName) {
|
|
814
|
-
tlsSessionCache.set(sessionIdKey, {
|
|
815
|
-
domain: serverName,
|
|
816
|
-
sessionId: sessionId,
|
|
817
|
-
ticketTimestamp: Date.now()
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
if (enableLogging) {
|
|
821
|
-
console.log(`Stored session ${sessionIdKey} for domain ${serverName}`);
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
// Also store session ticket information if present
|
|
826
|
-
if (sessionTicketId && serverName) {
|
|
827
|
-
tlsSessionCache.set(`ticket:${sessionTicketId}`, {
|
|
828
|
-
domain: serverName,
|
|
829
|
-
ticketId: sessionTicketId,
|
|
830
|
-
ticketTimestamp: Date.now()
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
if (enableLogging) {
|
|
834
|
-
console.log(`Stored session ticket ${sessionTicketId} for domain ${serverName}`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Return the complete extraction result
|
|
839
|
-
return {
|
|
840
|
-
serverName,
|
|
841
|
-
sessionId,
|
|
842
|
-
sessionIdKey,
|
|
843
|
-
sessionTicketId,
|
|
844
|
-
isResumption,
|
|
845
|
-
resumedDomain,
|
|
846
|
-
hasSessionTicket
|
|
847
|
-
};
|
|
250
|
+
return serverName;
|
|
848
251
|
}
|
|
849
|
-
|
|
850
|
-
// Skip this name entry
|
|
252
|
+
|
|
851
253
|
offset += nameLen;
|
|
852
254
|
}
|
|
853
|
-
|
|
854
|
-
// Finished processing the SNI extension without finding a hostname
|
|
855
255
|
break;
|
|
856
256
|
} else {
|
|
857
|
-
// Skip other extensions
|
|
858
257
|
offset += extensionLength;
|
|
859
258
|
}
|
|
860
259
|
}
|
|
861
|
-
|
|
862
|
-
// We finished processing all extensions without finding SNI
|
|
260
|
+
|
|
863
261
|
if (enableLogging) console.log('No SNI extension found');
|
|
864
|
-
|
|
865
|
-
// Even without SNI, we might be dealing with a session resumption
|
|
866
|
-
if (isResumption && resumedDomain) {
|
|
867
|
-
return {
|
|
868
|
-
serverName: resumedDomain, // Use the domain from previous session
|
|
869
|
-
sessionId,
|
|
870
|
-
sessionIdKey,
|
|
871
|
-
sessionTicketId,
|
|
872
|
-
hasSessionTicket,
|
|
873
|
-
isResumption: true,
|
|
874
|
-
resumedDomain
|
|
875
|
-
};
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
// Return a basic result with just the session info
|
|
879
|
-
return {
|
|
880
|
-
isResumption,
|
|
881
|
-
sessionId,
|
|
882
|
-
sessionIdKey,
|
|
883
|
-
sessionTicketId,
|
|
884
|
-
hasSessionTicket,
|
|
885
|
-
resumedDomain
|
|
886
|
-
};
|
|
262
|
+
return undefined;
|
|
887
263
|
} catch (err) {
|
|
888
|
-
console.log(`Error
|
|
264
|
+
console.log(`Error extracting SNI: ${err}`);
|
|
889
265
|
return undefined;
|
|
890
266
|
}
|
|
891
267
|
}
|
|
892
268
|
|
|
893
|
-
/**
|
|
894
|
-
* Legacy wrapper for extractSNIInfo to maintain backward compatibility
|
|
895
|
-
* @param buffer - Buffer containing the TLS ClientHello
|
|
896
|
-
* @param enableLogging - Whether to enable detailed logging
|
|
897
|
-
* @returns The server name if found, otherwise undefined
|
|
898
|
-
*/
|
|
899
|
-
function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | undefined {
|
|
900
|
-
const result = extractSNIInfo(buffer, enableLogging);
|
|
901
|
-
return result?.serverName;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
269
|
// Helper: Check if a port falls within any of the given port ranges
|
|
905
270
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
|
906
271
|
return ranges.some((range) => port >= range.from && port <= range.to);
|
|
@@ -963,22 +328,7 @@ const randomizeTimeout = (baseTimeout: number, variationPercent: number = 5): nu
|
|
|
963
328
|
|
|
964
329
|
export class PortProxy {
|
|
965
330
|
private netServers: plugins.net.Server[] = [];
|
|
966
|
-
|
|
967
|
-
// Define the internal settings interface to include all fields, including those removed from the public interface
|
|
968
|
-
settings: IPortProxySettings & {
|
|
969
|
-
// Internal fields removed from public interface in 3.31.0+
|
|
970
|
-
initialDataTimeout: number;
|
|
971
|
-
socketTimeout: number;
|
|
972
|
-
inactivityCheckInterval: number;
|
|
973
|
-
maxConnectionLifetime: number;
|
|
974
|
-
inactivityTimeout: number;
|
|
975
|
-
disableInactivityCheck: boolean;
|
|
976
|
-
enableKeepAliveProbes: boolean;
|
|
977
|
-
keepAliveTreatment: 'standard' | 'extended' | 'immortal';
|
|
978
|
-
keepAliveInactivityMultiplier: number;
|
|
979
|
-
extendedKeepAliveLifetime: number;
|
|
980
|
-
};
|
|
981
|
-
|
|
331
|
+
settings: IPortProxySettings;
|
|
982
332
|
private connectionRecords: Map<string, IConnectionRecord> = new Map();
|
|
983
333
|
private connectionLogger: NodeJS.Timeout | null = null;
|
|
984
334
|
private isShuttingDown: boolean = false;
|
|
@@ -998,116 +348,51 @@ export class PortProxy {
|
|
|
998
348
|
// Connection tracking by IP for rate limiting
|
|
999
349
|
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
1000
350
|
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
1001
|
-
|
|
351
|
+
|
|
1002
352
|
// New property to store NetworkProxy instances
|
|
1003
353
|
private networkProxies: NetworkProxy[] = [];
|
|
1004
354
|
|
|
1005
355
|
constructor(settingsArg: IPortProxySettings) {
|
|
1006
|
-
//
|
|
1007
|
-
const targetIP = settingsArg.targetIP || 'localhost';
|
|
1008
|
-
const isChainedProxy = settingsArg.isChainedProxy !== undefined
|
|
1009
|
-
? settingsArg.isChainedProxy
|
|
1010
|
-
: (targetIP === 'localhost' || targetIP === '127.0.0.1');
|
|
1011
|
-
|
|
1012
|
-
// Use more aggressive timeouts for chained proxies
|
|
1013
|
-
const aggressiveTlsRefresh = settingsArg.aggressiveTlsRefresh !== undefined
|
|
1014
|
-
? settingsArg.aggressiveTlsRefresh
|
|
1015
|
-
: isChainedProxy;
|
|
1016
|
-
|
|
1017
|
-
// Configure TLS session cache if specified
|
|
1018
|
-
if (settingsArg.tlsSessionCache) {
|
|
1019
|
-
tlsSessionCache.updateConfig({
|
|
1020
|
-
enabled: settingsArg.tlsSessionCache.enabled,
|
|
1021
|
-
maxEntries: settingsArg.tlsSessionCache.maxEntries,
|
|
1022
|
-
expiryTime: settingsArg.tlsSessionCache.expiryTime,
|
|
1023
|
-
cleanupInterval: settingsArg.tlsSessionCache.cleanupInterval
|
|
1024
|
-
});
|
|
1025
|
-
|
|
1026
|
-
console.log(`Configured TLS session cache with custom settings. Current stats: ${JSON.stringify(tlsSessionCache.getStats())}`);
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
// Determine appropriate timeouts based on proxy chain position
|
|
1030
|
-
// Much more relaxed socket timeouts
|
|
1031
|
-
let socketTimeout = 6 * 60 * 60 * 1000; // 6 hours default for standalone
|
|
1032
|
-
|
|
1033
|
-
if (isChainedProxy) {
|
|
1034
|
-
// Still adjust based on chain position, but with more relaxed values
|
|
1035
|
-
const chainPosition = settingsArg.chainPosition || 'middle';
|
|
1036
|
-
|
|
1037
|
-
// Adjust timeouts based on position in chain, but significantly relaxed
|
|
1038
|
-
switch (chainPosition) {
|
|
1039
|
-
case 'first':
|
|
1040
|
-
// First proxy handling browser connections
|
|
1041
|
-
socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
|
|
1042
|
-
break;
|
|
1043
|
-
case 'middle':
|
|
1044
|
-
// Middle proxies
|
|
1045
|
-
socketTimeout = 5 * 60 * 60 * 1000; // 5 hours
|
|
1046
|
-
break;
|
|
1047
|
-
case 'last':
|
|
1048
|
-
// Last proxy connects to backend
|
|
1049
|
-
socketTimeout = 6 * 60 * 60 * 1000; // 6 hours
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
console.log(`Configured as ${chainPosition} proxy in chain. Using relaxed timeouts for better stability.`);
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
// Set sensible defaults with significantly relaxed timeouts
|
|
356
|
+
// Set reasonable defaults for all settings
|
|
1057
357
|
this.settings = {
|
|
1058
358
|
...settingsArg,
|
|
1059
|
-
targetIP: targetIP,
|
|
1060
|
-
|
|
1061
|
-
//
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
//
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
inactivityCheckInterval: 5 * 60 * 1000, // 5 minutes between checks (relaxed)
|
|
1070
|
-
maxConnectionLifetime: 12 * 60 * 60 * 1000, // 12 hours lifetime
|
|
1071
|
-
inactivityTimeout: 4 * 60 * 60 * 1000, // 4 hours inactivity timeout
|
|
1072
|
-
|
|
1073
|
-
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 60000, // 60 seconds
|
|
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
|
+
|
|
368
|
+
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
|
|
1074
369
|
|
|
1075
370
|
// Socket optimization settings
|
|
1076
371
|
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
1077
372
|
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
1078
|
-
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay ||
|
|
1079
|
-
maxPendingDataSize: settingsArg.maxPendingDataSize ||
|
|
373
|
+
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds (reduced for responsiveness)
|
|
374
|
+
maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
|
|
1080
375
|
|
|
1081
|
-
// Feature flags
|
|
1082
|
-
disableInactivityCheck: false,
|
|
1083
|
-
enableKeepAliveProbes:
|
|
376
|
+
// Feature flags
|
|
377
|
+
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
378
|
+
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined
|
|
379
|
+
? settingsArg.enableKeepAliveProbes : true, // Enable by default
|
|
1084
380
|
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
1085
381
|
enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
|
|
1086
|
-
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
|
|
382
|
+
enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, // Disable randomization by default
|
|
1087
383
|
|
|
1088
384
|
// Rate limiting defaults
|
|
1089
|
-
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP ||
|
|
1090
|
-
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute ||
|
|
1091
|
-
|
|
1092
|
-
//
|
|
1093
|
-
keepAliveTreatment: 'extended', //
|
|
1094
|
-
keepAliveInactivityMultiplier:
|
|
1095
|
-
|
|
1096
|
-
extendedKeepAliveLifetime: isChainedProxy
|
|
1097
|
-
? 24 * 60 * 60 * 1000 // 24 hours for chained proxies
|
|
1098
|
-
: 48 * 60 * 60 * 1000, // 48 hours for standalone proxies
|
|
385
|
+
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
|
386
|
+
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
|
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
|
|
1099
392
|
};
|
|
1100
|
-
|
|
393
|
+
|
|
1101
394
|
// Store NetworkProxy instances if provided
|
|
1102
395
|
this.networkProxies = settingsArg.networkProxies || [];
|
|
1103
|
-
|
|
1104
|
-
// Log proxy configuration details
|
|
1105
|
-
console.log(`PortProxy initialized with ${isChainedProxy ? 'chained proxy' : 'standalone'} configuration.`);
|
|
1106
|
-
if (isChainedProxy) {
|
|
1107
|
-
console.log(`TLS certificate refresh: ${aggressiveTlsRefresh ? 'Aggressive' : 'Standard'}`);
|
|
1108
|
-
console.log(`Connection lifetime: ${plugins.prettyMs(this.settings.maxConnectionLifetime)}`);
|
|
1109
|
-
console.log(`Inactivity timeout: ${plugins.prettyMs(this.settings.inactivityTimeout)}`);
|
|
1110
|
-
}
|
|
1111
396
|
}
|
|
1112
397
|
|
|
1113
398
|
/**
|
|
@@ -1128,93 +413,58 @@ export class PortProxy {
|
|
|
1128
413
|
serverName?: string
|
|
1129
414
|
): void {
|
|
1130
415
|
// Determine which NetworkProxy to use
|
|
1131
|
-
const proxyIndex =
|
|
1132
|
-
|
|
1133
|
-
|
|
416
|
+
const proxyIndex = domainConfig.networkProxyIndex !== undefined
|
|
417
|
+
? domainConfig.networkProxyIndex
|
|
418
|
+
: 0;
|
|
419
|
+
|
|
1134
420
|
// Validate the NetworkProxy index
|
|
1135
421
|
if (proxyIndex < 0 || proxyIndex >= this.networkProxies.length) {
|
|
1136
|
-
console.log(
|
|
1137
|
-
`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`
|
|
1138
|
-
);
|
|
422
|
+
console.log(`[${connectionId}] Invalid NetworkProxy index: ${proxyIndex}. Using fallback direct connection.`);
|
|
1139
423
|
// Fall back to direct connection
|
|
1140
|
-
return this.setupDirectConnection(
|
|
1141
|
-
connectionId,
|
|
1142
|
-
socket,
|
|
1143
|
-
record,
|
|
1144
|
-
domainConfig,
|
|
1145
|
-
serverName,
|
|
1146
|
-
initialData
|
|
1147
|
-
);
|
|
424
|
+
return this.setupDirectConnection(connectionId, socket, record, domainConfig, serverName, initialData);
|
|
1148
425
|
}
|
|
1149
|
-
|
|
426
|
+
|
|
1150
427
|
const networkProxy = this.networkProxies[proxyIndex];
|
|
1151
428
|
const proxyPort = networkProxy.getListeningPort();
|
|
1152
429
|
const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
|
|
1153
|
-
|
|
430
|
+
|
|
1154
431
|
if (this.settings.enableDetailedLogging) {
|
|
1155
432
|
console.log(
|
|
1156
433
|
`[${connectionId}] Forwarding TLS connection to NetworkProxy[${proxyIndex}] at ${proxyHost}:${proxyPort}`
|
|
1157
434
|
);
|
|
1158
435
|
}
|
|
1159
|
-
|
|
1160
|
-
// Create a connection to the NetworkProxy
|
|
436
|
+
|
|
437
|
+
// Create a connection to the NetworkProxy
|
|
1161
438
|
const proxySocket = plugins.net.connect({
|
|
1162
439
|
host: proxyHost,
|
|
1163
|
-
port: proxyPort
|
|
1164
|
-
noDelay: true, // Disable Nagle's algorithm for NetworkProxy connections
|
|
1165
|
-
keepAlive: this.settings.keepAlive, // Use the same keepAlive setting as regular connections
|
|
1166
|
-
keepAliveInitialDelay: Math.max(this.settings.keepAliveInitialDelay - 5000, 5000) // Slightly faster
|
|
440
|
+
port: proxyPort
|
|
1167
441
|
});
|
|
1168
|
-
|
|
442
|
+
|
|
1169
443
|
// Store the outgoing socket in the record
|
|
1170
444
|
record.outgoing = proxySocket;
|
|
1171
445
|
record.outgoingStartTime = Date.now();
|
|
1172
446
|
record.usingNetworkProxy = true;
|
|
1173
447
|
record.networkProxyIndex = proxyIndex;
|
|
1174
448
|
|
|
1175
|
-
// Mark keep-alive as enabled on outgoing if requested
|
|
1176
|
-
if (this.settings.keepAlive) {
|
|
1177
|
-
record.outgoingKeepAliveEnabled = true;
|
|
1178
|
-
|
|
1179
|
-
// Apply enhanced TCP keep-alive options if enabled
|
|
1180
|
-
if (this.settings.enableKeepAliveProbes) {
|
|
1181
|
-
try {
|
|
1182
|
-
if ('setKeepAliveProbes' in proxySocket) {
|
|
1183
|
-
(proxySocket as any).setKeepAliveProbes(10);
|
|
1184
|
-
}
|
|
1185
|
-
if ('setKeepAliveInterval' in proxySocket) {
|
|
1186
|
-
(proxySocket as any).setKeepAliveInterval(800);
|
|
1187
|
-
}
|
|
1188
|
-
|
|
1189
|
-
console.log(`[${connectionId}] Enhanced TCP keep-alive configured for NetworkProxy connection`);
|
|
1190
|
-
} catch (err) {
|
|
1191
|
-
// Ignore errors - these are optional enhancements
|
|
1192
|
-
if (this.settings.enableDetailedLogging) {
|
|
1193
|
-
console.log(`[${connectionId}] Enhanced keep-alive not supported for NetworkProxy: ${err}`);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
449
|
// Set up error handlers
|
|
1200
450
|
proxySocket.on('error', (err) => {
|
|
1201
451
|
console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
|
|
1202
452
|
this.cleanupConnection(record, 'network_proxy_connect_error');
|
|
1203
453
|
});
|
|
1204
|
-
|
|
454
|
+
|
|
1205
455
|
// Handle connection to NetworkProxy
|
|
1206
456
|
proxySocket.on('connect', () => {
|
|
1207
457
|
if (this.settings.enableDetailedLogging) {
|
|
1208
458
|
console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
|
|
1209
459
|
}
|
|
1210
|
-
|
|
460
|
+
|
|
1211
461
|
// First send the initial data that contains the TLS ClientHello
|
|
1212
462
|
proxySocket.write(initialData);
|
|
1213
|
-
|
|
463
|
+
|
|
1214
464
|
// Now set up bidirectional piping between client and NetworkProxy
|
|
1215
465
|
socket.pipe(proxySocket);
|
|
1216
466
|
proxySocket.pipe(socket);
|
|
1217
|
-
|
|
467
|
+
|
|
1218
468
|
// Setup cleanup handlers
|
|
1219
469
|
proxySocket.on('close', () => {
|
|
1220
470
|
if (this.settings.enableDetailedLogging) {
|
|
@@ -1222,63 +472,18 @@ export class PortProxy {
|
|
|
1222
472
|
}
|
|
1223
473
|
this.cleanupConnection(record, 'network_proxy_closed');
|
|
1224
474
|
});
|
|
1225
|
-
|
|
475
|
+
|
|
1226
476
|
socket.on('close', () => {
|
|
1227
477
|
if (this.settings.enableDetailedLogging) {
|
|
1228
|
-
console.log(
|
|
1229
|
-
`[${connectionId}] Client connection closed after forwarding to NetworkProxy`
|
|
1230
|
-
);
|
|
478
|
+
console.log(`[${connectionId}] Client connection closed after forwarding to NetworkProxy`);
|
|
1231
479
|
}
|
|
1232
480
|
this.cleanupConnection(record, 'client_closed');
|
|
1233
481
|
});
|
|
1234
482
|
|
|
1235
|
-
//
|
|
1236
|
-
socket.on('data', (
|
|
1237
|
-
// Check for TLS handshake packets (ContentType.handshake)
|
|
1238
|
-
if (chunk.length > 0 && chunk[0] === 22) {
|
|
1239
|
-
console.log(`[${connectionId}] Detected potential TLS handshake with NetworkProxy, updating activity`);
|
|
1240
|
-
this.updateActivity(record);
|
|
1241
|
-
}
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
// Update activity on data transfer from the proxy socket
|
|
483
|
+
// Update activity on data transfer
|
|
484
|
+
socket.on('data', () => this.updateActivity(record));
|
|
1245
485
|
proxySocket.on('data', () => this.updateActivity(record));
|
|
1246
486
|
|
|
1247
|
-
// Special handling for application-level keep-alives on NetworkProxy connections
|
|
1248
|
-
if (this.settings.keepAlive && record.isTLS) {
|
|
1249
|
-
// Set up a timer to periodically send application-level keep-alives
|
|
1250
|
-
const keepAliveTimer = setInterval(() => {
|
|
1251
|
-
if (proxySocket && !proxySocket.destroyed && record && !record.connectionClosed) {
|
|
1252
|
-
try {
|
|
1253
|
-
// Send 0-byte packet as application-level keep-alive
|
|
1254
|
-
proxySocket.write(Buffer.alloc(0));
|
|
1255
|
-
|
|
1256
|
-
if (this.settings.enableDetailedLogging) {
|
|
1257
|
-
console.log(`[${connectionId}] Sent application-level keep-alive to NetworkProxy connection`);
|
|
1258
|
-
}
|
|
1259
|
-
} catch (err) {
|
|
1260
|
-
// If we can't write, the connection is probably already dead
|
|
1261
|
-
if (this.settings.enableDetailedLogging) {
|
|
1262
|
-
console.log(`[${connectionId}] Error sending application-level keep-alive to NetworkProxy: ${err}`);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// Stop the timer if we hit an error
|
|
1266
|
-
clearInterval(keepAliveTimer);
|
|
1267
|
-
}
|
|
1268
|
-
} else {
|
|
1269
|
-
// Clean up timer if connection is gone
|
|
1270
|
-
clearInterval(keepAliveTimer);
|
|
1271
|
-
}
|
|
1272
|
-
}, 60000); // Send keep-alive every minute
|
|
1273
|
-
|
|
1274
|
-
// Make sure interval doesn't prevent process exit
|
|
1275
|
-
if (keepAliveTimer.unref) {
|
|
1276
|
-
keepAliveTimer.unref();
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
console.log(`[${connectionId}] Application-level keep-alive configured for NetworkProxy connection`);
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
487
|
if (this.settings.enableDetailedLogging) {
|
|
1283
488
|
console.log(
|
|
1284
489
|
`[${connectionId}] TLS connection successfully forwarded to NetworkProxy[${proxyIndex}]`
|
|
@@ -1286,7 +491,7 @@ export class PortProxy {
|
|
|
1286
491
|
}
|
|
1287
492
|
});
|
|
1288
493
|
}
|
|
1289
|
-
|
|
494
|
+
|
|
1290
495
|
/**
|
|
1291
496
|
* Sets up a direct connection to the target (original behavior)
|
|
1292
497
|
* This is used when NetworkProxy isn't configured or as a fallback
|
|
@@ -1300,37 +505,15 @@ export class PortProxy {
|
|
|
1300
505
|
initialChunk?: Buffer,
|
|
1301
506
|
overridePort?: number
|
|
1302
507
|
): void {
|
|
1303
|
-
// Enhanced logging for initial connection troubleshooting
|
|
1304
|
-
if (serverName) {
|
|
1305
|
-
console.log(`[${connectionId}] Setting up direct connection for domain: ${serverName}`);
|
|
1306
|
-
} else {
|
|
1307
|
-
console.log(`[${connectionId}] Setting up direct connection without SNI`);
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
// Log domain config details to help diagnose routing issues
|
|
1311
|
-
if (domainConfig) {
|
|
1312
|
-
console.log(`[${connectionId}] Using domain config: ${domainConfig.domains.join(', ')}`);
|
|
1313
|
-
} else {
|
|
1314
|
-
console.log(`[${connectionId}] No specific domain config found, using default settings`);
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
// Ensure we maximize connection chances by setting appropriate timeouts
|
|
1318
|
-
socket.setTimeout(30000); // 30 second initial connect timeout
|
|
1319
|
-
|
|
1320
508
|
// Existing connection setup logic
|
|
1321
509
|
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
|
1322
510
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
|
1323
511
|
host: targetHost,
|
|
1324
512
|
port: overridePort !== undefined ? overridePort : this.settings.toPort,
|
|
1325
|
-
// Add connection timeout to ensure we don't hang indefinitely
|
|
1326
|
-
timeout: 15000 // 15 second connection timeout
|
|
1327
513
|
};
|
|
1328
514
|
if (this.settings.preserveSourceIP) {
|
|
1329
515
|
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
|
|
1330
516
|
}
|
|
1331
|
-
|
|
1332
|
-
console.log(`[${connectionId}] Connecting to backend: ${targetHost}:${connectionOptions.port}`);
|
|
1333
|
-
|
|
1334
517
|
|
|
1335
518
|
// Pause the incoming socket to prevent buffer overflows
|
|
1336
519
|
socket.pause();
|
|
@@ -1371,22 +554,11 @@ export class PortProxy {
|
|
|
1371
554
|
// Add the temp handler to capture all incoming data during connection setup
|
|
1372
555
|
socket.on('data', tempDataHandler);
|
|
1373
556
|
|
|
1374
|
-
// Add initial chunk to pending data if present
|
|
557
|
+
// Add initial chunk to pending data if present
|
|
1375
558
|
if (initialChunk) {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
record.
|
|
1379
|
-
record.pendingData.push(initialDataCopy);
|
|
1380
|
-
record.pendingDataSize = initialDataCopy.length;
|
|
1381
|
-
|
|
1382
|
-
// Log TLS handshake for debug purposes
|
|
1383
|
-
if (isTlsHandshake(initialChunk)) {
|
|
1384
|
-
record.isTLS = true;
|
|
1385
|
-
console.log(`[${connectionId}] Buffered TLS handshake data: ${initialDataCopy.length} bytes, SNI: ${serverName || 'none'}`);
|
|
1386
|
-
}
|
|
1387
|
-
} else if (record.isTLS) {
|
|
1388
|
-
// This shouldn't happen, but log a warning if we have a TLS connection with no initial data
|
|
1389
|
-
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;
|
|
1390
562
|
}
|
|
1391
563
|
|
|
1392
564
|
// Create the target socket but don't set up piping immediately
|
|
@@ -1396,77 +568,30 @@ export class PortProxy {
|
|
|
1396
568
|
|
|
1397
569
|
// Apply socket optimizations
|
|
1398
570
|
targetSocket.setNoDelay(this.settings.noDelay);
|
|
1399
|
-
|
|
571
|
+
|
|
1400
572
|
// Apply keep-alive settings to the outgoing connection as well
|
|
1401
573
|
if (this.settings.keepAlive) {
|
|
1402
|
-
|
|
1403
|
-
const outgoingInitialDelay = Math.max(this.settings.keepAliveInitialDelay - 5000, 5000);
|
|
1404
|
-
targetSocket.setKeepAlive(true, outgoingInitialDelay);
|
|
1405
|
-
record.outgoingKeepAliveEnabled = true;
|
|
574
|
+
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
1406
575
|
|
|
1407
|
-
console.log(`[${connectionId}] Keep-alive enabled on outgoing connection with initial delay: ${outgoingInitialDelay}ms`);
|
|
1408
|
-
|
|
1409
576
|
// Apply enhanced TCP keep-alive options if enabled
|
|
1410
577
|
if (this.settings.enableKeepAliveProbes) {
|
|
1411
578
|
try {
|
|
1412
579
|
if ('setKeepAliveProbes' in targetSocket) {
|
|
1413
|
-
(targetSocket as any).setKeepAliveProbes(10);
|
|
580
|
+
(targetSocket as any).setKeepAliveProbes(10);
|
|
1414
581
|
}
|
|
1415
582
|
if ('setKeepAliveInterval' in targetSocket) {
|
|
1416
|
-
|
|
1417
|
-
(targetSocket as any).setKeepAliveInterval(800); // Slightly faster than incoming
|
|
583
|
+
(targetSocket as any).setKeepAliveInterval(1000);
|
|
1418
584
|
}
|
|
1419
|
-
|
|
1420
|
-
console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on outgoing connection`);
|
|
1421
585
|
} catch (err) {
|
|
1422
586
|
// Ignore errors - these are optional enhancements
|
|
1423
587
|
if (this.settings.enableDetailedLogging) {
|
|
1424
|
-
console.log(
|
|
1425
|
-
`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
|
|
1426
|
-
);
|
|
588
|
+
console.log(`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`);
|
|
1427
589
|
}
|
|
1428
590
|
}
|
|
1429
591
|
}
|
|
1430
|
-
|
|
1431
|
-
// Special handling for TLS keep-alive - we want to be more aggressive
|
|
1432
|
-
// with keeping the outgoing connection alive in TLS mode
|
|
1433
|
-
if (record.isTLS) {
|
|
1434
|
-
// Set a timer to periodically send empty data to keep connection alive
|
|
1435
|
-
// This is in addition to TCP keep-alive, works at application layer
|
|
1436
|
-
const keepAliveTimer = setInterval(() => {
|
|
1437
|
-
if (targetSocket && !targetSocket.destroyed && record && !record.connectionClosed) {
|
|
1438
|
-
try {
|
|
1439
|
-
// Send 0-byte packet as application-level keep-alive
|
|
1440
|
-
targetSocket.write(Buffer.alloc(0));
|
|
1441
|
-
|
|
1442
|
-
if (this.settings.enableDetailedLogging) {
|
|
1443
|
-
console.log(`[${connectionId}] Sent application-level keep-alive to outgoing TLS connection`);
|
|
1444
|
-
}
|
|
1445
|
-
} catch (err) {
|
|
1446
|
-
// If we can't write, the connection is probably already dead
|
|
1447
|
-
if (this.settings.enableDetailedLogging) {
|
|
1448
|
-
console.log(`[${connectionId}] Error sending application-level keep-alive: ${err}`);
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
// Stop the timer if we hit an error
|
|
1452
|
-
clearInterval(keepAliveTimer);
|
|
1453
|
-
}
|
|
1454
|
-
} else {
|
|
1455
|
-
// Clean up timer if connection is gone
|
|
1456
|
-
clearInterval(keepAliveTimer);
|
|
1457
|
-
}
|
|
1458
|
-
}, 60000); // Send keep-alive every minute
|
|
1459
|
-
|
|
1460
|
-
// Make sure interval doesn't prevent process exit
|
|
1461
|
-
if (keepAliveTimer.unref) {
|
|
1462
|
-
keepAliveTimer.unref();
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
console.log(`[${connectionId}] Application-level keep-alive configured for TLS outgoing connection`);
|
|
1466
|
-
}
|
|
1467
592
|
}
|
|
1468
593
|
|
|
1469
|
-
// Setup specific error handler for connection phase
|
|
594
|
+
// Setup specific error handler for connection phase
|
|
1470
595
|
targetSocket.once('error', (err) => {
|
|
1471
596
|
// This handler runs only once during the initial connection phase
|
|
1472
597
|
const code = (err as any).code;
|
|
@@ -1477,7 +602,6 @@ export class PortProxy {
|
|
|
1477
602
|
// Resume the incoming socket to prevent it from hanging
|
|
1478
603
|
socket.resume();
|
|
1479
604
|
|
|
1480
|
-
// Add detailed logging for connection problems
|
|
1481
605
|
if (code === 'ECONNREFUSED') {
|
|
1482
606
|
console.log(
|
|
1483
607
|
`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
|
|
@@ -1493,28 +617,6 @@ export class PortProxy {
|
|
|
1493
617
|
} else if (code === 'EHOSTUNREACH') {
|
|
1494
618
|
console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
|
|
1495
619
|
}
|
|
1496
|
-
|
|
1497
|
-
// Log additional diagnostics
|
|
1498
|
-
console.log(`[${connectionId}] Connection details - SNI: ${serverName || 'none'}, HasChunk: ${!!initialChunk}, ChunkSize: ${initialChunk ? initialChunk.length : 0}`);
|
|
1499
|
-
|
|
1500
|
-
// For TLS connections, provide even more detailed diagnostics
|
|
1501
|
-
if (record.isTLS) {
|
|
1502
|
-
console.log(`[${connectionId}] TLS connection failure details - TLS detected: ${record.isTLS}, Server: ${targetHost}:${connectionOptions.port}, Domain config: ${domainConfig ? 'Present' : 'Missing'}`);
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
// For connection refusal or timeouts, try a more aggressive error response
|
|
1506
|
-
// This helps browsers quickly realize there's an issue rather than waiting
|
|
1507
|
-
if (code === 'ECONNREFUSED' || code === 'ETIMEDOUT' || code === 'EHOSTUNREACH') {
|
|
1508
|
-
try {
|
|
1509
|
-
// Send a RST packet rather than a graceful close
|
|
1510
|
-
// This signals to browsers to try a new connection immediately
|
|
1511
|
-
socket.destroy(new Error(`Backend connection failed: ${code}`));
|
|
1512
|
-
console.log(`[${connectionId}] Forced connection termination to trigger immediate browser retry`);
|
|
1513
|
-
return; // Skip normal cleanup
|
|
1514
|
-
} catch (destroyErr) {
|
|
1515
|
-
console.log(`[${connectionId}] Error during forced connection termination: ${destroyErr}`);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
620
|
|
|
1519
621
|
// Clear any existing error handler after connection phase
|
|
1520
622
|
targetSocket.removeAllListeners('error');
|
|
@@ -1540,21 +642,19 @@ export class PortProxy {
|
|
|
1540
642
|
// For keep-alive connections, just log a warning instead of closing
|
|
1541
643
|
if (record.hasKeepAlive) {
|
|
1542
644
|
console.log(
|
|
1543
|
-
`[${connectionId}] Timeout event on incoming keep-alive connection from ${
|
|
1544
|
-
record.remoteIP
|
|
1545
|
-
} after ${plugins.prettyMs(
|
|
645
|
+
`[${connectionId}] Timeout event on incoming keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
|
|
1546
646
|
this.settings.socketTimeout || 3600000
|
|
1547
647
|
)}. Connection preserved.`
|
|
1548
648
|
);
|
|
1549
649
|
// Don't close the connection - just log
|
|
1550
650
|
return;
|
|
1551
651
|
}
|
|
1552
|
-
|
|
652
|
+
|
|
1553
653
|
// For non-keep-alive connections, proceed with normal cleanup
|
|
1554
654
|
console.log(
|
|
1555
|
-
`[${connectionId}] Timeout on incoming side from ${
|
|
1556
|
-
|
|
1557
|
-
|
|
655
|
+
`[${connectionId}] Timeout on incoming side from ${record.remoteIP} after ${plugins.prettyMs(
|
|
656
|
+
this.settings.socketTimeout || 3600000
|
|
657
|
+
)}`
|
|
1558
658
|
);
|
|
1559
659
|
if (record.incomingTerminationReason === null) {
|
|
1560
660
|
record.incomingTerminationReason = 'timeout';
|
|
@@ -1567,21 +667,19 @@ export class PortProxy {
|
|
|
1567
667
|
// For keep-alive connections, just log a warning instead of closing
|
|
1568
668
|
if (record.hasKeepAlive) {
|
|
1569
669
|
console.log(
|
|
1570
|
-
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${
|
|
1571
|
-
record.remoteIP
|
|
1572
|
-
} after ${plugins.prettyMs(
|
|
670
|
+
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${record.remoteIP} after ${plugins.prettyMs(
|
|
1573
671
|
this.settings.socketTimeout || 3600000
|
|
1574
672
|
)}. Connection preserved.`
|
|
1575
673
|
);
|
|
1576
674
|
// Don't close the connection - just log
|
|
1577
675
|
return;
|
|
1578
676
|
}
|
|
1579
|
-
|
|
677
|
+
|
|
1580
678
|
// For non-keep-alive connections, proceed with normal cleanup
|
|
1581
679
|
console.log(
|
|
1582
|
-
`[${connectionId}] Timeout on outgoing side from ${
|
|
1583
|
-
|
|
1584
|
-
|
|
680
|
+
`[${connectionId}] Timeout on outgoing side from ${record.remoteIP} after ${plugins.prettyMs(
|
|
681
|
+
this.settings.socketTimeout || 3600000
|
|
682
|
+
)}`
|
|
1585
683
|
);
|
|
1586
684
|
if (record.outgoingTerminationReason === null) {
|
|
1587
685
|
record.outgoingTerminationReason = 'timeout';
|
|
@@ -1595,11 +693,9 @@ export class PortProxy {
|
|
|
1595
693
|
// Disable timeouts completely for immortal connections
|
|
1596
694
|
socket.setTimeout(0);
|
|
1597
695
|
targetSocket.setTimeout(0);
|
|
1598
|
-
|
|
696
|
+
|
|
1599
697
|
if (this.settings.enableDetailedLogging) {
|
|
1600
|
-
console.log(
|
|
1601
|
-
`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`
|
|
1602
|
-
);
|
|
698
|
+
console.log(`[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`);
|
|
1603
699
|
}
|
|
1604
700
|
} else {
|
|
1605
701
|
// Set normal timeouts for other connections
|
|
@@ -1627,153 +723,14 @@ export class PortProxy {
|
|
|
1627
723
|
// Flush all pending data to target
|
|
1628
724
|
if (record.pendingData.length > 0) {
|
|
1629
725
|
const combinedData = Buffer.concat(record.pendingData);
|
|
1630
|
-
|
|
1631
|
-
// Add critical debugging for SNI forwarding issues
|
|
1632
|
-
if (record.isTLS && this.settings.enableTlsDebugLogging) {
|
|
1633
|
-
console.log(`[${connectionId}] Forwarding TLS handshake data: ${combinedData.length} bytes, SNI: ${serverName || 'none'}`);
|
|
1634
|
-
|
|
1635
|
-
// Additional check to verify we're forwarding the ClientHello properly
|
|
1636
|
-
if (combinedData[0] === 22) { // TLS handshake
|
|
1637
|
-
console.log(`[${connectionId}] Initial data is a TLS handshake record`);
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// Write the combined data to the target
|
|
1642
726
|
targetSocket.write(combinedData, (err) => {
|
|
1643
727
|
if (err) {
|
|
1644
|
-
console.log(
|
|
728
|
+
console.log(
|
|
729
|
+
`[${connectionId}] Error writing pending data to target: ${err.message}`
|
|
730
|
+
);
|
|
1645
731
|
return this.initiateCleanupOnce(record, 'write_error');
|
|
1646
732
|
}
|
|
1647
|
-
|
|
1648
|
-
if (record.isTLS) {
|
|
1649
|
-
// Log successful forwarding of initial TLS data
|
|
1650
|
-
console.log(`[${connectionId}] Successfully forwarded initial TLS data to backend`);
|
|
1651
|
-
}
|
|
1652
733
|
|
|
1653
|
-
// Set up the renegotiation listener *before* piping if this is a TLS connection with SNI
|
|
1654
|
-
if (serverName && record.isTLS) {
|
|
1655
|
-
// Create a flag to prevent double-processing of the same handshake packet
|
|
1656
|
-
let processingRenegotiation = false;
|
|
1657
|
-
|
|
1658
|
-
// This listener handles TLS renegotiation detection on the incoming socket
|
|
1659
|
-
socket.on('data', (renegChunk) => {
|
|
1660
|
-
// Only check for content type 22 (handshake) and not already processing
|
|
1661
|
-
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22 && !processingRenegotiation) {
|
|
1662
|
-
processingRenegotiation = true;
|
|
1663
|
-
|
|
1664
|
-
// Always update activity timestamp for any handshake packet
|
|
1665
|
-
this.updateActivity(record);
|
|
1666
|
-
|
|
1667
|
-
try {
|
|
1668
|
-
// Enhanced logging for renegotiation
|
|
1669
|
-
console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
|
|
1670
|
-
|
|
1671
|
-
// Extract all TLS information including session resumption data
|
|
1672
|
-
const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1673
|
-
|
|
1674
|
-
// Log details about the handshake packet
|
|
1675
|
-
if (this.settings.enableTlsDebugLogging) {
|
|
1676
|
-
console.log(`[${connectionId}] Handshake SNI extraction results:`, {
|
|
1677
|
-
isResumption: sniInfo?.isResumption || false,
|
|
1678
|
-
serverName: sniInfo?.serverName || 'none',
|
|
1679
|
-
resumedDomain: sniInfo?.resumedDomain || 'none',
|
|
1680
|
-
recordsExamined: sniInfo?.recordsExamined || 0,
|
|
1681
|
-
multipleRecords: sniInfo?.multipleRecords || false,
|
|
1682
|
-
partialExtract: sniInfo?.partialExtract || false
|
|
1683
|
-
});
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
let newSNI = sniInfo?.serverName;
|
|
1687
|
-
|
|
1688
|
-
// Handle session resumption - if we recognize the session ID, we know what domain it belongs to
|
|
1689
|
-
if (sniInfo?.isResumption && sniInfo.resumedDomain) {
|
|
1690
|
-
console.log(`[${connectionId}] Rehandshake with session resumption for domain: ${sniInfo.resumedDomain}`);
|
|
1691
|
-
newSNI = sniInfo.resumedDomain;
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
// IMPORTANT: If we can't extract an SNI from renegotiation, but we detected a TLS handshake,
|
|
1695
|
-
// we still need to make sure it's properly forwarded to maintain the TLS state
|
|
1696
|
-
if (newSNI === undefined) {
|
|
1697
|
-
console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
|
|
1698
|
-
|
|
1699
|
-
// Set a temporary timeout to reset the processing flag
|
|
1700
|
-
setTimeout(() => {
|
|
1701
|
-
processingRenegotiation = false;
|
|
1702
|
-
}, 500);
|
|
1703
|
-
|
|
1704
|
-
return; // Let the piping handle the forwarding
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
// Check if the SNI has changed
|
|
1708
|
-
if (newSNI !== serverName) {
|
|
1709
|
-
console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
|
|
1710
|
-
|
|
1711
|
-
// Allow if the new SNI matches existing domain config or find a new matching config
|
|
1712
|
-
let allowed = false;
|
|
1713
|
-
|
|
1714
|
-
if (record.domainConfig) {
|
|
1715
|
-
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
if (!allowed) {
|
|
1719
|
-
const newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
1720
|
-
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
|
1721
|
-
);
|
|
1722
|
-
|
|
1723
|
-
if (newDomainConfig) {
|
|
1724
|
-
const effectiveAllowedIPs = [
|
|
1725
|
-
...newDomainConfig.allowedIPs,
|
|
1726
|
-
...(this.settings.defaultAllowedIPs || []),
|
|
1727
|
-
];
|
|
1728
|
-
const effectiveBlockedIPs = [
|
|
1729
|
-
...(newDomainConfig.blockedIPs || []),
|
|
1730
|
-
...(this.settings.defaultBlockedIPs || []),
|
|
1731
|
-
];
|
|
1732
|
-
|
|
1733
|
-
allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
|
|
1734
|
-
|
|
1735
|
-
if (allowed) {
|
|
1736
|
-
record.domainConfig = newDomainConfig;
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
|
|
1741
|
-
if (allowed) {
|
|
1742
|
-
console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
|
|
1743
|
-
record.lockedDomain = newSNI;
|
|
1744
|
-
} else {
|
|
1745
|
-
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
|
|
1746
|
-
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
1747
|
-
return;
|
|
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
|
-
} finally {
|
|
1755
|
-
// Reset the processing flag after a small delay to prevent double-processing
|
|
1756
|
-
// of packets that may be part of the same handshake
|
|
1757
|
-
setTimeout(() => {
|
|
1758
|
-
processingRenegotiation = false;
|
|
1759
|
-
}, 500);
|
|
1760
|
-
}
|
|
1761
|
-
}
|
|
1762
|
-
});
|
|
1763
|
-
|
|
1764
|
-
// Set up a listener on the outgoing socket to detect issues with renegotiation
|
|
1765
|
-
// This helps catch cases where the outgoing connection has closed but the incoming is still active
|
|
1766
|
-
targetSocket.on('error', (err) => {
|
|
1767
|
-
// If we get an error during what might be a renegotiation, log it specially
|
|
1768
|
-
if (processingRenegotiation) {
|
|
1769
|
-
console.log(`[${connectionId}] ERROR: Outgoing socket error during TLS renegotiation: ${err.message}`);
|
|
1770
|
-
// Force immediate cleanup to prevent hanging connections
|
|
1771
|
-
this.initiateCleanupOnce(record, 'renegotiation_error');
|
|
1772
|
-
}
|
|
1773
|
-
// The normal error handler will be called for other errors
|
|
1774
|
-
});
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
734
|
// Now set up piping for future data and resume the socket
|
|
1778
735
|
socket.pipe(targetSocket);
|
|
1779
736
|
targetSocket.pipe(socket);
|
|
@@ -1789,9 +746,7 @@ export class PortProxy {
|
|
|
1789
746
|
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
1790
747
|
: ''
|
|
1791
748
|
}` +
|
|
1792
|
-
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
1793
|
-
record.hasKeepAlive ? 'Yes' : 'No'
|
|
1794
|
-
}`
|
|
749
|
+
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
1795
750
|
);
|
|
1796
751
|
} else {
|
|
1797
752
|
console.log(
|
|
@@ -1803,177 +758,11 @@ export class PortProxy {
|
|
|
1803
758
|
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
1804
759
|
: ''
|
|
1805
760
|
}`
|
|
1806
|
-
);
|
|
1807
|
-
}
|
|
1808
|
-
});
|
|
1809
|
-
} else {
|
|
1810
|
-
//
|
|
1811
|
-
if (serverName && record.isTLS) {
|
|
1812
|
-
// Create a flag to prevent double-processing of the same handshake packet
|
|
1813
|
-
let processingRenegotiation = false;
|
|
1814
|
-
|
|
1815
|
-
// This listener handles TLS renegotiation detection on the incoming socket
|
|
1816
|
-
socket.on('data', (renegChunk) => {
|
|
1817
|
-
// Only check for content type 22 (handshake) and not already processing
|
|
1818
|
-
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22 && !processingRenegotiation) {
|
|
1819
|
-
processingRenegotiation = true;
|
|
1820
|
-
|
|
1821
|
-
// Always update activity timestamp for any handshake packet
|
|
1822
|
-
this.updateActivity(record);
|
|
1823
|
-
|
|
1824
|
-
try {
|
|
1825
|
-
// Enhanced logging for renegotiation
|
|
1826
|
-
console.log(`[${connectionId}] TLS handshake/renegotiation packet detected (${renegChunk.length} bytes)`);
|
|
1827
|
-
|
|
1828
|
-
// Extract all TLS information including session resumption data
|
|
1829
|
-
const sniInfo = extractSNIInfo(renegChunk, this.settings.enableTlsDebugLogging);
|
|
1830
|
-
|
|
1831
|
-
// Log details about the handshake packet
|
|
1832
|
-
if (this.settings.enableTlsDebugLogging) {
|
|
1833
|
-
console.log(`[${connectionId}] Handshake SNI extraction results:`, {
|
|
1834
|
-
isResumption: sniInfo?.isResumption || false,
|
|
1835
|
-
serverName: sniInfo?.serverName || 'none',
|
|
1836
|
-
resumedDomain: sniInfo?.resumedDomain || 'none',
|
|
1837
|
-
recordsExamined: sniInfo?.recordsExamined || 0,
|
|
1838
|
-
multipleRecords: sniInfo?.multipleRecords || false,
|
|
1839
|
-
partialExtract: sniInfo?.partialExtract || false
|
|
1840
|
-
});
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
let newSNI = sniInfo?.serverName;
|
|
1844
|
-
|
|
1845
|
-
// Handle session resumption - if we recognize the session ID, we know what domain it belongs to
|
|
1846
|
-
if (sniInfo?.isResumption && sniInfo.resumedDomain) {
|
|
1847
|
-
console.log(`[${connectionId}] Rehandshake with session resumption for domain: ${sniInfo.resumedDomain}`);
|
|
1848
|
-
newSNI = sniInfo.resumedDomain;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
// IMPORTANT: If we can't extract an SNI from renegotiation, but we detected a TLS handshake,
|
|
1852
|
-
// we still need to make sure it's properly forwarded to maintain the TLS state
|
|
1853
|
-
if (newSNI === undefined) {
|
|
1854
|
-
console.log(`[${connectionId}] Rehandshake detected without SNI, forwarding transparently.`);
|
|
1855
|
-
|
|
1856
|
-
// Set a temporary timeout to reset the processing flag
|
|
1857
|
-
setTimeout(() => {
|
|
1858
|
-
processingRenegotiation = false;
|
|
1859
|
-
}, 500);
|
|
1860
|
-
|
|
1861
|
-
return; // Let the piping handle the forwarding
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
// Check if the SNI has changed
|
|
1865
|
-
if (newSNI !== serverName) {
|
|
1866
|
-
console.log(`[${connectionId}] Rehandshake with different SNI: ${newSNI} vs original ${serverName}`);
|
|
1867
|
-
|
|
1868
|
-
// Allow if the new SNI matches existing domain config or find a new matching config
|
|
1869
|
-
let allowed = false;
|
|
1870
|
-
|
|
1871
|
-
// First check if the new SNI is allowed under the existing domain config
|
|
1872
|
-
// This is the preferred approach as it maintains the existing connection context
|
|
1873
|
-
if (record.domainConfig) {
|
|
1874
|
-
allowed = record.domainConfig.domains.some(d => plugins.minimatch(newSNI, d));
|
|
1875
|
-
|
|
1876
|
-
if (allowed) {
|
|
1877
|
-
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} allowed by existing domain config`);
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
// If not allowed by existing config, try to find an alternative domain config
|
|
1882
|
-
if (!allowed) {
|
|
1883
|
-
// First try exact match
|
|
1884
|
-
let newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
1885
|
-
config.domains.some((d) => plugins.minimatch(newSNI, d))
|
|
1886
|
-
);
|
|
1887
|
-
|
|
1888
|
-
// If no exact match, try flexible matching with domain parts (for wildcard domains)
|
|
1889
|
-
if (!newDomainConfig) {
|
|
1890
|
-
console.log(`[${connectionId}] No exact domain config match for rehandshake SNI: ${newSNI}, trying flexible matching`);
|
|
1891
|
-
|
|
1892
|
-
const domainParts = newSNI.split('.');
|
|
1893
|
-
|
|
1894
|
-
// Try matching with parent domains or wildcard patterns
|
|
1895
|
-
if (domainParts.length > 2) {
|
|
1896
|
-
const parentDomain = domainParts.slice(1).join('.');
|
|
1897
|
-
const wildcardDomain = '*.' + parentDomain;
|
|
1898
|
-
|
|
1899
|
-
console.log(`[${connectionId}] Trying alternative patterns: ${parentDomain} or ${wildcardDomain}`);
|
|
1900
|
-
|
|
1901
|
-
newDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
1902
|
-
config.domains.some((d) =>
|
|
1903
|
-
d === parentDomain ||
|
|
1904
|
-
d === wildcardDomain ||
|
|
1905
|
-
plugins.minimatch(parentDomain, d)
|
|
1906
|
-
)
|
|
1907
|
-
);
|
|
1908
|
-
}
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
if (newDomainConfig) {
|
|
1912
|
-
const effectiveAllowedIPs = [
|
|
1913
|
-
...newDomainConfig.allowedIPs,
|
|
1914
|
-
...(this.settings.defaultAllowedIPs || []),
|
|
1915
|
-
];
|
|
1916
|
-
const effectiveBlockedIPs = [
|
|
1917
|
-
...(newDomainConfig.blockedIPs || []),
|
|
1918
|
-
...(this.settings.defaultBlockedIPs || []),
|
|
1919
|
-
];
|
|
1920
|
-
|
|
1921
|
-
allowed = isGlobIPAllowed(record.remoteIP, effectiveAllowedIPs, effectiveBlockedIPs);
|
|
1922
|
-
|
|
1923
|
-
if (allowed) {
|
|
1924
|
-
record.domainConfig = newDomainConfig;
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
|
|
1929
|
-
if (allowed) {
|
|
1930
|
-
console.log(`[${connectionId}] Updated domain for connection from ${record.remoteIP} to: ${newSNI}`);
|
|
1931
|
-
record.lockedDomain = newSNI;
|
|
1932
|
-
} else {
|
|
1933
|
-
console.log(`[${connectionId}] Rehandshake SNI ${newSNI} not allowed. Terminating connection.`);
|
|
1934
|
-
this.initiateCleanupOnce(record, 'sni_mismatch');
|
|
1935
|
-
return;
|
|
1936
|
-
}
|
|
1937
|
-
} else {
|
|
1938
|
-
console.log(`[${connectionId}] Rehandshake with same SNI: ${newSNI}`);
|
|
1939
|
-
}
|
|
1940
|
-
} catch (err) {
|
|
1941
|
-
console.log(`[${connectionId}] Error processing renegotiation: ${err}. Allowing to continue.`);
|
|
1942
|
-
} finally {
|
|
1943
|
-
// Reset the processing flag after a small delay to prevent double-processing
|
|
1944
|
-
// of packets that may be part of the same handshake
|
|
1945
|
-
setTimeout(() => {
|
|
1946
|
-
processingRenegotiation = false;
|
|
1947
|
-
}, 500);
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
});
|
|
1951
|
-
|
|
1952
|
-
// Set up a listener on the outgoing socket to detect issues with renegotiation
|
|
1953
|
-
// This helps catch cases where the outgoing connection has closed but the incoming is still active
|
|
1954
|
-
targetSocket.on('error', (err) => {
|
|
1955
|
-
// If we get an error during what might be a renegotiation, log it specially
|
|
1956
|
-
if (processingRenegotiation) {
|
|
1957
|
-
console.log(`[${connectionId}] ERROR: Outgoing socket error during TLS renegotiation: ${err.message}`);
|
|
1958
|
-
// Force immediate cleanup to prevent hanging connections
|
|
1959
|
-
this.initiateCleanupOnce(record, 'renegotiation_error');
|
|
1960
|
-
}
|
|
1961
|
-
// The normal error handler will be called for other errors
|
|
1962
|
-
});
|
|
1963
|
-
|
|
1964
|
-
// Also monitor targetSocket for connection issues during client handshakes
|
|
1965
|
-
targetSocket.on('close', () => {
|
|
1966
|
-
// If the outgoing socket closes during renegotiation, it's a critical issue
|
|
1967
|
-
if (processingRenegotiation) {
|
|
1968
|
-
console.log(`[${connectionId}] CRITICAL: Outgoing socket closed during TLS renegotiation!`);
|
|
1969
|
-
console.log(`[${connectionId}] This likely explains cert mismatch errors in the browser.`);
|
|
1970
|
-
// Force immediate cleanup on the client side
|
|
1971
|
-
this.initiateCleanupOnce(record, 'target_closed_during_renegotiation');
|
|
1972
|
-
}
|
|
1973
|
-
});
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
// Now set up piping
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
} else {
|
|
765
|
+
// No pending data, so just set up piping
|
|
1977
766
|
socket.pipe(targetSocket);
|
|
1978
767
|
targetSocket.pipe(socket);
|
|
1979
768
|
socket.resume(); // Resume the socket after piping is established
|
|
@@ -1988,9 +777,7 @@ export class PortProxy {
|
|
|
1988
777
|
? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
|
|
1989
778
|
: ''
|
|
1990
779
|
}` +
|
|
1991
|
-
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
1992
|
-
record.hasKeepAlive ? 'Yes' : 'No'
|
|
1993
|
-
}`
|
|
780
|
+
` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}`
|
|
1994
781
|
);
|
|
1995
782
|
} else {
|
|
1996
783
|
console.log(
|
|
@@ -2010,98 +797,82 @@ export class PortProxy {
|
|
|
2010
797
|
record.pendingData = [];
|
|
2011
798
|
record.pendingDataSize = 0;
|
|
2012
799
|
|
|
2013
|
-
//
|
|
2014
|
-
|
|
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
|
+
}
|
|
2015
825
|
|
|
2016
826
|
// Set connection timeout with simpler logic
|
|
2017
827
|
if (record.cleanupTimer) {
|
|
2018
828
|
clearTimeout(record.cleanupTimer);
|
|
2019
829
|
}
|
|
2020
|
-
|
|
830
|
+
|
|
2021
831
|
// For immortal keep-alive connections, skip setting a timeout completely
|
|
2022
832
|
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
|
|
2023
833
|
if (this.settings.enableDetailedLogging) {
|
|
2024
|
-
console.log(
|
|
2025
|
-
`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
|
|
2026
|
-
);
|
|
834
|
+
console.log(`[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`);
|
|
2027
835
|
}
|
|
2028
836
|
// No cleanup timer for immortal connections
|
|
2029
|
-
}
|
|
2030
|
-
// For TLS keep-alive connections, use a more generous timeout now that
|
|
2031
|
-
// we've fixed the renegotiation handling issue that was causing certificate problems
|
|
2032
|
-
else if (record.hasKeepAlive && record.isTLS) {
|
|
2033
|
-
// Use a longer timeout for TLS connections now that renegotiation handling is fixed
|
|
2034
|
-
// This reduces unnecessary reconnections while still ensuring certificate freshness
|
|
2035
|
-
const tlsKeepAliveTimeout = 4 * 60 * 60 * 1000; // 4 hours for TLS keep-alive - increased from 30 minutes
|
|
2036
|
-
const safeTimeout = ensureSafeTimeout(tlsKeepAliveTimeout);
|
|
2037
|
-
|
|
2038
|
-
record.cleanupTimer = setTimeout(() => {
|
|
2039
|
-
console.log(
|
|
2040
|
-
`[${connectionId}] TLS keep-alive connection from ${
|
|
2041
|
-
record.remoteIP
|
|
2042
|
-
} exceeded max lifetime (${plugins.prettyMs(
|
|
2043
|
-
tlsKeepAliveTimeout
|
|
2044
|
-
)}), forcing cleanup to refresh certificate context.`
|
|
2045
|
-
);
|
|
2046
|
-
this.initiateCleanupOnce(record, 'tls_certificate_refresh');
|
|
2047
|
-
}, safeTimeout);
|
|
2048
|
-
|
|
2049
|
-
// Make sure timeout doesn't keep the process alive
|
|
2050
|
-
if (record.cleanupTimer.unref) {
|
|
2051
|
-
record.cleanupTimer.unref();
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
if (this.settings.enableDetailedLogging) {
|
|
2055
|
-
console.log(
|
|
2056
|
-
`[${connectionId}] TLS keep-alive connection with aggressive certificate refresh protection, lifetime: ${plugins.prettyMs(
|
|
2057
|
-
tlsKeepAliveTimeout
|
|
2058
|
-
)}`
|
|
2059
|
-
);
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
837
|
+
}
|
|
2062
838
|
// For extended keep-alive connections, use extended timeout
|
|
2063
839
|
else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
2064
840
|
const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
2065
841
|
const safeTimeout = ensureSafeTimeout(extendedTimeout);
|
|
2066
|
-
|
|
842
|
+
|
|
2067
843
|
record.cleanupTimer = setTimeout(() => {
|
|
2068
844
|
console.log(
|
|
2069
|
-
`[${connectionId}] Keep-alive connection from ${
|
|
2070
|
-
|
|
2071
|
-
|
|
845
|
+
`[${connectionId}] Keep-alive connection from ${record.remoteIP} exceeded extended lifetime (${plugins.prettyMs(
|
|
846
|
+
extendedTimeout
|
|
847
|
+
)}), forcing cleanup.`
|
|
2072
848
|
);
|
|
2073
849
|
this.initiateCleanupOnce(record, 'extended_lifetime');
|
|
2074
850
|
}, safeTimeout);
|
|
2075
|
-
|
|
851
|
+
|
|
2076
852
|
// Make sure timeout doesn't keep the process alive
|
|
2077
853
|
if (record.cleanupTimer.unref) {
|
|
2078
854
|
record.cleanupTimer.unref();
|
|
2079
855
|
}
|
|
2080
|
-
|
|
856
|
+
|
|
2081
857
|
if (this.settings.enableDetailedLogging) {
|
|
2082
|
-
console.log(
|
|
2083
|
-
`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
|
|
2084
|
-
extendedTimeout
|
|
2085
|
-
)}`
|
|
2086
|
-
);
|
|
858
|
+
console.log(`[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(extendedTimeout)}`);
|
|
2087
859
|
}
|
|
2088
860
|
}
|
|
2089
861
|
// For standard connections, use normal timeout
|
|
2090
862
|
else {
|
|
2091
863
|
// Use domain-specific timeout if available, otherwise use default
|
|
2092
|
-
const connectionTimeout =
|
|
2093
|
-
record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
|
|
864
|
+
const connectionTimeout = record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
|
|
2094
865
|
const safeTimeout = ensureSafeTimeout(connectionTimeout);
|
|
2095
|
-
|
|
866
|
+
|
|
2096
867
|
record.cleanupTimer = setTimeout(() => {
|
|
2097
868
|
console.log(
|
|
2098
|
-
`[${connectionId}] Connection from ${
|
|
2099
|
-
|
|
2100
|
-
|
|
869
|
+
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime (${plugins.prettyMs(
|
|
870
|
+
connectionTimeout
|
|
871
|
+
)}), forcing cleanup.`
|
|
2101
872
|
);
|
|
2102
873
|
this.initiateCleanupOnce(record, 'connection_timeout');
|
|
2103
874
|
}, safeTimeout);
|
|
2104
|
-
|
|
875
|
+
|
|
2105
876
|
// Make sure timeout doesn't keep the process alive
|
|
2106
877
|
if (record.cleanupTimer.unref) {
|
|
2107
878
|
record.cleanupTimer.unref();
|
|
@@ -2179,220 +950,6 @@ export class PortProxy {
|
|
|
2179
950
|
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
|
|
2180
951
|
}
|
|
2181
952
|
|
|
2182
|
-
/**
|
|
2183
|
-
* Update connection activity timestamp with enhanced sleep detection
|
|
2184
|
-
* Improved for chained proxy scenarios and more aggressive handling of stale connections
|
|
2185
|
-
*/
|
|
2186
|
-
private updateActivity(record: IConnectionRecord): void {
|
|
2187
|
-
// Get the current time
|
|
2188
|
-
const now = Date.now();
|
|
2189
|
-
|
|
2190
|
-
// Check if there was a large time gap that suggests system sleep
|
|
2191
|
-
if (record.lastActivity > 0) {
|
|
2192
|
-
const timeDiff = now - record.lastActivity;
|
|
2193
|
-
|
|
2194
|
-
// Enhanced sleep detection with graduated thresholds - much more relaxed
|
|
2195
|
-
// Using chain detection from settings instead of recalculating
|
|
2196
|
-
const isChainedProxy = this.settings.isChainedProxy || false;
|
|
2197
|
-
const minuteInMs = 60 * 1000;
|
|
2198
|
-
const hourInMs = 60 * minuteInMs;
|
|
2199
|
-
|
|
2200
|
-
// Significantly relaxed thresholds for better stability
|
|
2201
|
-
const shortInactivityThreshold = 30 * minuteInMs; // 30 minutes
|
|
2202
|
-
const mediumInactivityThreshold = 2 * hourInMs; // 2 hours
|
|
2203
|
-
const longInactivityThreshold = 8 * hourInMs; // 8 hours
|
|
2204
|
-
|
|
2205
|
-
// Short inactivity (10-15 mins) - Might be temporary network issue or short sleep
|
|
2206
|
-
if (timeDiff > shortInactivityThreshold) {
|
|
2207
|
-
if (record.isTLS && !record.possibleSystemSleep) {
|
|
2208
|
-
// Record first detection of possible sleep/inactivity
|
|
2209
|
-
record.possibleSystemSleep = true;
|
|
2210
|
-
record.lastSleepDetection = now;
|
|
2211
|
-
|
|
2212
|
-
if (this.settings.enableDetailedLogging) {
|
|
2213
|
-
console.log(
|
|
2214
|
-
`[${record.id}] Detected possible short inactivity for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2215
|
-
`Monitoring for TLS connection health.`
|
|
2216
|
-
);
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
// For TLS connections, send a minimal probe to check connection health
|
|
2220
|
-
if (!record.usingNetworkProxy && record.outgoing && !record.outgoing.destroyed) {
|
|
2221
|
-
try {
|
|
2222
|
-
record.outgoing.write(Buffer.alloc(0));
|
|
2223
|
-
} catch (err) {
|
|
2224
|
-
console.log(`[${record.id}] Error sending TLS probe: ${err}`);
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
// Medium inactivity (20-30 mins) - Likely a sleep event or network change
|
|
2231
|
-
if (timeDiff > mediumInactivityThreshold && record.hasKeepAlive) {
|
|
2232
|
-
console.log(
|
|
2233
|
-
`[${record.id}] Detected medium inactivity period for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2234
|
-
`Taking proactive steps for connection health.`
|
|
2235
|
-
);
|
|
2236
|
-
|
|
2237
|
-
// For TLS connections, we need more aggressive handling
|
|
2238
|
-
if (record.isTLS && record.tlsHandshakeComplete) {
|
|
2239
|
-
// If in a chained proxy, we should be even more aggressive about refreshing
|
|
2240
|
-
if (isChainedProxy) {
|
|
2241
|
-
console.log(
|
|
2242
|
-
`[${record.id}] TLS connection in chained proxy inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2243
|
-
`Closing to prevent certificate inconsistencies across chain.`
|
|
2244
|
-
);
|
|
2245
|
-
return this.initiateCleanupOnce(record, 'chained_proxy_inactivity');
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
// For TLS in single proxy, try refresh first
|
|
2249
|
-
console.log(
|
|
2250
|
-
`[${record.id}] TLS connection inactive for ${plugins.prettyMs(timeDiff)}. ` +
|
|
2251
|
-
`Attempting active refresh of TLS state.`
|
|
2252
|
-
);
|
|
2253
|
-
|
|
2254
|
-
// Attempt deep TLS state refresh with buffer flush
|
|
2255
|
-
this.performDeepTlsRefresh(record);
|
|
2256
|
-
|
|
2257
|
-
// Schedule verification check with tighter timing for chained setups
|
|
2258
|
-
const verificationTimeout = isChainedProxy ? 5 * minuteInMs : 10 * minuteInMs;
|
|
2259
|
-
const refreshCheckId = record.id;
|
|
2260
|
-
const refreshCheck = setTimeout(() => {
|
|
2261
|
-
const currentRecord = this.connectionRecords.get(refreshCheckId);
|
|
2262
|
-
if (currentRecord) {
|
|
2263
|
-
const verificationTimeDiff = Date.now() - currentRecord.lastActivity;
|
|
2264
|
-
if (verificationTimeDiff > verificationTimeout / 2) {
|
|
2265
|
-
console.log(
|
|
2266
|
-
`[${refreshCheckId}] No activity detected after TLS refresh (${plugins.prettyMs(verificationTimeDiff)}). ` +
|
|
2267
|
-
`Closing connection to ensure proper browser reconnection.`
|
|
2268
|
-
);
|
|
2269
|
-
this.initiateCleanupOnce(currentRecord, 'tls_refresh_verification_failed');
|
|
2270
|
-
}
|
|
2271
|
-
}
|
|
2272
|
-
}, verificationTimeout);
|
|
2273
|
-
|
|
2274
|
-
// Make sure timeout doesn't keep the process alive
|
|
2275
|
-
if (refreshCheck.unref) {
|
|
2276
|
-
refreshCheck.unref();
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
// Update sleep detection markers
|
|
2281
|
-
record.possibleSystemSleep = true;
|
|
2282
|
-
record.lastSleepDetection = now;
|
|
2283
|
-
}
|
|
2284
|
-
|
|
2285
|
-
// Long inactivity (60-120 mins) - Definite sleep/suspend or major network change
|
|
2286
|
-
if (timeDiff > longInactivityThreshold) {
|
|
2287
|
-
console.log(
|
|
2288
|
-
`[${record.id}] Detected long inactivity period of ${plugins.prettyMs(timeDiff)}. ` +
|
|
2289
|
-
`Closing connection to ensure fresh certificate context.`
|
|
2290
|
-
);
|
|
2291
|
-
|
|
2292
|
-
// For long periods, we always want to force close and let browser reconnect
|
|
2293
|
-
// This ensures fresh certificates and proper TLS context across the chain
|
|
2294
|
-
return this.initiateCleanupOnce(record, 'extended_inactivity_refresh');
|
|
2295
|
-
}
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
// Update the activity timestamp
|
|
2299
|
-
record.lastActivity = now;
|
|
2300
|
-
|
|
2301
|
-
// Clear any inactivity warning
|
|
2302
|
-
if (record.inactivityWarningIssued) {
|
|
2303
|
-
record.inactivityWarningIssued = false;
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
/**
|
|
2308
|
-
* Perform deep TLS state refresh after sleep detection
|
|
2309
|
-
* More aggressive than the standard refresh, specifically designed for
|
|
2310
|
-
* recovering connections after system sleep in chained proxy setups
|
|
2311
|
-
*/
|
|
2312
|
-
private performDeepTlsRefresh(record: IConnectionRecord): void {
|
|
2313
|
-
// Skip if we're using a NetworkProxy as it handles its own TLS state
|
|
2314
|
-
if (record.usingNetworkProxy) {
|
|
2315
|
-
return;
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
try {
|
|
2319
|
-
// For outgoing connections that might need to be refreshed
|
|
2320
|
-
if (record.outgoing && !record.outgoing.destroyed) {
|
|
2321
|
-
// Check how long this connection has been established
|
|
2322
|
-
const connectionAge = Date.now() - record.incomingStartTime;
|
|
2323
|
-
const hourInMs = 60 * 60 * 1000;
|
|
2324
|
-
|
|
2325
|
-
// For very long-lived connections, just close them
|
|
2326
|
-
if (connectionAge > 4 * hourInMs) { // Reduced from 8 hours to 4 hours for chained proxies
|
|
2327
|
-
console.log(
|
|
2328
|
-
`[${record.id}] Long-lived TLS connection (${plugins.prettyMs(connectionAge)}). ` +
|
|
2329
|
-
`Closing to ensure proper certificate handling across proxy chain.`
|
|
2330
|
-
);
|
|
2331
|
-
return this.initiateCleanupOnce(record, 'certificate_age_refresh');
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
// Perform a series of actions to try to refresh the TLS state
|
|
2335
|
-
|
|
2336
|
-
// 1. Send a zero-length buffer to trigger any pending errors
|
|
2337
|
-
record.outgoing.write(Buffer.alloc(0));
|
|
2338
|
-
|
|
2339
|
-
// 2. Check socket state
|
|
2340
|
-
if (record.outgoing.writableEnded || !record.outgoing.writable) {
|
|
2341
|
-
console.log(`[${record.id}] Socket no longer writable during refresh`);
|
|
2342
|
-
return this.initiateCleanupOnce(record, 'socket_state_error');
|
|
2343
|
-
}
|
|
2344
|
-
|
|
2345
|
-
// 3. For TLS connections, try to force background renegotiation
|
|
2346
|
-
// by manipulating socket timeouts
|
|
2347
|
-
const originalTimeout = record.outgoing.timeout;
|
|
2348
|
-
record.outgoing.setTimeout(100); // Set very short timeout
|
|
2349
|
-
|
|
2350
|
-
// 4. Create a small delay to allow timeout to process
|
|
2351
|
-
setTimeout(() => {
|
|
2352
|
-
try {
|
|
2353
|
-
if (record.outgoing && !record.outgoing.destroyed) {
|
|
2354
|
-
// Reset timeout to original value
|
|
2355
|
-
record.outgoing.setTimeout(originalTimeout || 0);
|
|
2356
|
-
|
|
2357
|
-
// Send another probe with random data (16 bytes) that will be ignored by TLS layer
|
|
2358
|
-
// but might trigger internal state updates in the TLS implementation
|
|
2359
|
-
const probeBuffer = Buffer.alloc(16);
|
|
2360
|
-
// Fill with random data
|
|
2361
|
-
for (let i = 0; i < 16; i++) {
|
|
2362
|
-
probeBuffer[i] = Math.floor(Math.random() * 256);
|
|
2363
|
-
}
|
|
2364
|
-
record.outgoing.write(Buffer.alloc(0));
|
|
2365
|
-
|
|
2366
|
-
if (this.settings.enableDetailedLogging) {
|
|
2367
|
-
console.log(`[${record.id}] Completed deep TLS refresh sequence`);
|
|
2368
|
-
}
|
|
2369
|
-
}
|
|
2370
|
-
} catch (innerErr) {
|
|
2371
|
-
console.log(`[${record.id}] Error during deep TLS refresh: ${innerErr}`);
|
|
2372
|
-
this.initiateCleanupOnce(record, 'deep_refresh_error');
|
|
2373
|
-
}
|
|
2374
|
-
}, 150);
|
|
2375
|
-
|
|
2376
|
-
if (this.settings.enableDetailedLogging) {
|
|
2377
|
-
console.log(`[${record.id}] Initiated deep TLS refresh sequence`);
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2380
|
-
} catch (err) {
|
|
2381
|
-
console.log(`[${record.id}] Error starting TLS state refresh: ${err}`);
|
|
2382
|
-
|
|
2383
|
-
// If we hit an error, it's likely the connection is already broken
|
|
2384
|
-
// Force cleanup to ensure browser reconnects cleanly
|
|
2385
|
-
return this.initiateCleanupOnce(record, 'tls_refresh_error');
|
|
2386
|
-
}
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
/**
|
|
2390
|
-
* Legacy refresh method for backward compatibility
|
|
2391
|
-
*/
|
|
2392
|
-
private refreshTlsStateAfterSleep(record: IConnectionRecord): void {
|
|
2393
|
-
return this.performDeepTlsRefresh(record);
|
|
2394
|
-
}
|
|
2395
|
-
|
|
2396
953
|
/**
|
|
2397
954
|
* Cleans up a connection record.
|
|
2398
955
|
* Destroys both incoming and outgoing sockets, clears timers, and removes the record.
|
|
@@ -2490,9 +1047,7 @@ export class PortProxy {
|
|
|
2490
1047
|
` Duration: ${plugins.prettyMs(
|
|
2491
1048
|
duration
|
|
2492
1049
|
)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
2493
|
-
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
|
|
2494
|
-
record.hasKeepAlive ? 'Yes' : 'No'
|
|
2495
|
-
}` +
|
|
1050
|
+
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
|
|
2496
1051
|
`${record.usingNetworkProxy ? `, NetworkProxy: ${record.networkProxyIndex}` : ''}`
|
|
2497
1052
|
);
|
|
2498
1053
|
} else {
|
|
@@ -2503,6 +1058,18 @@ export class PortProxy {
|
|
|
2503
1058
|
}
|
|
2504
1059
|
}
|
|
2505
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
|
+
|
|
2506
1073
|
/**
|
|
2507
1074
|
* Get target IP with round-robin support
|
|
2508
1075
|
*/
|
|
@@ -2515,7 +1082,7 @@ export class PortProxy {
|
|
|
2515
1082
|
}
|
|
2516
1083
|
return this.settings.targetIP!;
|
|
2517
1084
|
}
|
|
2518
|
-
|
|
1085
|
+
|
|
2519
1086
|
/**
|
|
2520
1087
|
* Initiates cleanup once for a connection
|
|
2521
1088
|
*/
|
|
@@ -2523,15 +1090,12 @@ export class PortProxy {
|
|
|
2523
1090
|
if (this.settings.enableDetailedLogging) {
|
|
2524
1091
|
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
|
|
2525
1092
|
}
|
|
2526
|
-
|
|
2527
|
-
if (
|
|
2528
|
-
record.incomingTerminationReason === null ||
|
|
2529
|
-
record.incomingTerminationReason === undefined
|
|
2530
|
-
) {
|
|
1093
|
+
|
|
1094
|
+
if (record.incomingTerminationReason === null || record.incomingTerminationReason === undefined) {
|
|
2531
1095
|
record.incomingTerminationReason = reason;
|
|
2532
1096
|
this.incrementTerminationStat('incoming', reason);
|
|
2533
1097
|
}
|
|
2534
|
-
|
|
1098
|
+
|
|
2535
1099
|
this.cleanupConnection(record, reason);
|
|
2536
1100
|
}
|
|
2537
1101
|
|
|
@@ -2655,7 +1219,7 @@ export class PortProxy {
|
|
|
2655
1219
|
|
|
2656
1220
|
// Apply socket optimizations
|
|
2657
1221
|
socket.setNoDelay(this.settings.noDelay);
|
|
2658
|
-
|
|
1222
|
+
|
|
2659
1223
|
// Create a unique connection ID and record
|
|
2660
1224
|
const connectionId = generateConnectionId();
|
|
2661
1225
|
const connectionRecord: IConnectionRecord = {
|
|
@@ -2679,27 +1243,16 @@ export class PortProxy {
|
|
|
2679
1243
|
hasKeepAlive: false, // Will set to true if keep-alive is applied
|
|
2680
1244
|
incomingTerminationReason: null,
|
|
2681
1245
|
outgoingTerminationReason: null,
|
|
2682
|
-
|
|
2683
|
-
// Initialize NetworkProxy tracking fields
|
|
2684
|
-
usingNetworkProxy: false,
|
|
2685
|
-
|
|
2686
|
-
// Initialize sleep detection fields
|
|
2687
|
-
possibleSystemSleep: false,
|
|
2688
1246
|
|
|
2689
|
-
//
|
|
2690
|
-
|
|
2691
|
-
outgoingKeepAliveEnabled: false,
|
|
1247
|
+
// Initialize NetworkProxy tracking fields
|
|
1248
|
+
usingNetworkProxy: false
|
|
2692
1249
|
};
|
|
2693
|
-
|
|
1250
|
+
|
|
2694
1251
|
// Apply keep-alive settings if enabled
|
|
2695
1252
|
if (this.settings.keepAlive) {
|
|
2696
|
-
// Configure incoming socket keep-alive
|
|
2697
1253
|
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
2698
1254
|
connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
|
|
2699
|
-
connectionRecord.incomingKeepAliveEnabled = true;
|
|
2700
1255
|
|
|
2701
|
-
console.log(`[${connectionId}] Keep-alive enabled on incoming connection with initial delay: ${this.settings.keepAliveInitialDelay}ms`);
|
|
2702
|
-
|
|
2703
1256
|
// Apply enhanced TCP keep-alive options if enabled
|
|
2704
1257
|
if (this.settings.enableKeepAliveProbes) {
|
|
2705
1258
|
try {
|
|
@@ -2710,14 +1263,10 @@ export class PortProxy {
|
|
|
2710
1263
|
if ('setKeepAliveInterval' in socket) {
|
|
2711
1264
|
(socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
|
|
2712
1265
|
}
|
|
2713
|
-
|
|
2714
|
-
console.log(`[${connectionId}] Enhanced TCP keep-alive probes configured on incoming connection`);
|
|
2715
1266
|
} catch (err) {
|
|
2716
1267
|
// Ignore errors - these are optional enhancements
|
|
2717
1268
|
if (this.settings.enableDetailedLogging) {
|
|
2718
|
-
console.log(
|
|
2719
|
-
`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
|
|
2720
|
-
);
|
|
1269
|
+
console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
|
|
2721
1270
|
}
|
|
2722
1271
|
}
|
|
2723
1272
|
}
|
|
@@ -2730,8 +1279,8 @@ export class PortProxy {
|
|
|
2730
1279
|
if (this.settings.enableDetailedLogging) {
|
|
2731
1280
|
console.log(
|
|
2732
1281
|
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
|
|
2733
|
-
|
|
2734
|
-
|
|
1282
|
+
`Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
|
|
1283
|
+
`Active connections: ${this.connectionRecords.size}`
|
|
2735
1284
|
);
|
|
2736
1285
|
} else {
|
|
2737
1286
|
console.log(
|
|
@@ -2835,95 +1384,16 @@ export class PortProxy {
|
|
|
2835
1384
|
}
|
|
2836
1385
|
|
|
2837
1386
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
|
2838
|
-
|
|
1387
|
+
const domainConfig = forcedDomain
|
|
2839
1388
|
? forcedDomain
|
|
2840
1389
|
: serverName
|
|
2841
1390
|
? this.settings.domainConfigs.find((config) =>
|
|
2842
1391
|
config.domains.some((d) => plugins.minimatch(serverName, d))
|
|
2843
1392
|
)
|
|
2844
1393
|
: undefined;
|
|
2845
|
-
|
|
2846
|
-
// Enhanced logging to diagnose domain config selection issues
|
|
2847
|
-
if (serverName && !domainConfig) {
|
|
2848
|
-
console.log(`[${connectionId}] WARNING: No domain config found for SNI: ${serverName}`);
|
|
2849
|
-
console.log(`[${connectionId}] Available domains:`,
|
|
2850
|
-
this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
|
|
2851
|
-
} else if (serverName && domainConfig) {
|
|
2852
|
-
console.log(`[${connectionId}] Found domain config for SNI: ${serverName} -> ${domainConfig.domains.join(',')}`);
|
|
2853
|
-
}
|
|
2854
1394
|
|
|
2855
|
-
// For session resumption, ensure we use the domain config matching the resumed domain
|
|
2856
|
-
// The resumed domain will be in serverName if this is a session resumption
|
|
2857
|
-
if (serverName && connectionRecord.lockedDomain === serverName && serverName !== '') {
|
|
2858
|
-
// Override domain config lookup for session resumption - crucial for certificate selection
|
|
2859
|
-
|
|
2860
|
-
// First try an exact match
|
|
2861
|
-
let resumedDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
2862
|
-
config.domains.some((d) => plugins.minimatch(serverName, d))
|
|
2863
|
-
);
|
|
2864
|
-
|
|
2865
|
-
// If no exact match found, try a more flexible approach using domain parts
|
|
2866
|
-
if (!resumedDomainConfig) {
|
|
2867
|
-
console.log(`[${connectionId}] No exact domain config match for resumed domain: ${serverName}, trying flexible matching`);
|
|
2868
|
-
|
|
2869
|
-
// Extract domain parts (e.g., for "sub.example.com" try matching with "*.example.com")
|
|
2870
|
-
const domainParts = serverName.split('.');
|
|
2871
|
-
|
|
2872
|
-
// Try matching with parent domains or wildcard patterns
|
|
2873
|
-
if (domainParts.length > 2) {
|
|
2874
|
-
const parentDomain = domainParts.slice(1).join('.');
|
|
2875
|
-
const wildcardDomain = '*.' + parentDomain;
|
|
2876
|
-
|
|
2877
|
-
console.log(`[${connectionId}] Trying alternative patterns: ${parentDomain} or ${wildcardDomain}`);
|
|
2878
|
-
|
|
2879
|
-
resumedDomainConfig = this.settings.domainConfigs.find((config) =>
|
|
2880
|
-
config.domains.some((d) =>
|
|
2881
|
-
d === parentDomain ||
|
|
2882
|
-
d === wildcardDomain ||
|
|
2883
|
-
plugins.minimatch(parentDomain, d)
|
|
2884
|
-
)
|
|
2885
|
-
);
|
|
2886
|
-
}
|
|
2887
|
-
}
|
|
2888
|
-
|
|
2889
|
-
if (resumedDomainConfig) {
|
|
2890
|
-
domainConfig = resumedDomainConfig;
|
|
2891
|
-
console.log(`[${connectionId}] Found domain config for resumed session: ${serverName} -> ${resumedDomainConfig.domains.join(',')}`);
|
|
2892
|
-
} else {
|
|
2893
|
-
// As a fallback, use the first domain config with the same target IP if possible
|
|
2894
|
-
if (domainConfig && domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
|
|
2895
|
-
const targetIP = domainConfig.targetIPs[0];
|
|
2896
|
-
|
|
2897
|
-
const similarConfig = this.settings.domainConfigs.find((config) =>
|
|
2898
|
-
config.targetIPs && config.targetIPs.includes(targetIP)
|
|
2899
|
-
);
|
|
2900
|
-
|
|
2901
|
-
if (similarConfig && similarConfig !== domainConfig) {
|
|
2902
|
-
console.log(`[${connectionId}] Using similar domain config with matching target IP for resumed domain: ${serverName}`);
|
|
2903
|
-
domainConfig = similarConfig;
|
|
2904
|
-
} else {
|
|
2905
|
-
console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
|
|
2906
|
-
// Log available domains to help diagnose the issue
|
|
2907
|
-
console.log(`[${connectionId}] Available domains:`,
|
|
2908
|
-
this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
|
|
2909
|
-
}
|
|
2910
|
-
} else {
|
|
2911
|
-
console.log(`[${connectionId}] WARNING: Cannot find domain config for resumed domain: ${serverName}`);
|
|
2912
|
-
// Log available domains to help diagnose the issue
|
|
2913
|
-
console.log(`[${connectionId}] Available domains:`,
|
|
2914
|
-
this.settings.domainConfigs.map(config => config.domains.join(',')).join(' | '));
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
1395
|
// Save domain config in connection record
|
|
2920
1396
|
connectionRecord.domainConfig = domainConfig;
|
|
2921
|
-
|
|
2922
|
-
// Always set the lockedDomain, even for non-SNI connections
|
|
2923
|
-
if (serverName) {
|
|
2924
|
-
connectionRecord.lockedDomain = serverName;
|
|
2925
|
-
console.log(`[${connectionId}] Locked connection to domain: ${serverName}`);
|
|
2926
|
-
}
|
|
2927
1397
|
|
|
2928
1398
|
// IP validation is skipped if allowedIPs is empty
|
|
2929
1399
|
if (domainConfig) {
|
|
@@ -2948,12 +1418,12 @@ export class PortProxy {
|
|
|
2948
1418
|
)}`
|
|
2949
1419
|
);
|
|
2950
1420
|
}
|
|
2951
|
-
|
|
1421
|
+
|
|
2952
1422
|
// Check if we should forward this to a NetworkProxy
|
|
2953
1423
|
if (
|
|
2954
|
-
isTlsHandshakeDetected &&
|
|
2955
|
-
domainConfig.useNetworkProxy === true &&
|
|
2956
|
-
initialChunk &&
|
|
1424
|
+
isTlsHandshakeDetected &&
|
|
1425
|
+
domainConfig.useNetworkProxy === true &&
|
|
1426
|
+
initialChunk &&
|
|
2957
1427
|
this.networkProxies.length > 0
|
|
2958
1428
|
) {
|
|
2959
1429
|
return this.forwardToNetworkProxy(
|
|
@@ -3080,49 +1550,19 @@ export class PortProxy {
|
|
|
3080
1550
|
|
|
3081
1551
|
initialDataReceived = true;
|
|
3082
1552
|
|
|
3083
|
-
// Try to extract SNI
|
|
1553
|
+
// Try to extract SNI
|
|
3084
1554
|
let serverName = '';
|
|
3085
|
-
|
|
3086
|
-
// Record the chunk size for diagnostic purposes
|
|
3087
|
-
console.log(`[${connectionId}] Received initial data: ${chunk.length} bytes`);
|
|
3088
1555
|
|
|
3089
1556
|
if (isTlsHandshake(chunk)) {
|
|
3090
1557
|
connectionRecord.isTLS = true;
|
|
3091
1558
|
|
|
3092
|
-
console.log(`[${connectionId}] Detected TLS handshake`);
|
|
3093
|
-
|
|
3094
1559
|
if (this.settings.enableTlsDebugLogging) {
|
|
3095
1560
|
console.log(
|
|
3096
1561
|
`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
|
|
3097
1562
|
);
|
|
3098
1563
|
}
|
|
3099
1564
|
|
|
3100
|
-
|
|
3101
|
-
const sniInfo = extractSNIInfo(chunk, this.settings.enableTlsDebugLogging);
|
|
3102
|
-
|
|
3103
|
-
if (sniInfo?.isResumption && sniInfo.resumedDomain) {
|
|
3104
|
-
// This is a session resumption with a known domain
|
|
3105
|
-
serverName = sniInfo.resumedDomain;
|
|
3106
|
-
console.log(`[${connectionId}] TLS Session resumption detected for domain: ${serverName}`);
|
|
3107
|
-
|
|
3108
|
-
// When resuming a session, explicitly set the domain in the record to ensure proper routing
|
|
3109
|
-
// This is CRITICAL for ensuring we select the correct backend/certificate
|
|
3110
|
-
connectionRecord.lockedDomain = serverName;
|
|
3111
|
-
|
|
3112
|
-
// Force detailed logging for resumed sessions to help with troubleshooting
|
|
3113
|
-
console.log(`[${connectionId}] Resuming TLS session for domain ${serverName} - will use original certificate`);
|
|
3114
|
-
} else {
|
|
3115
|
-
// Normal SNI extraction
|
|
3116
|
-
serverName = sniInfo?.serverName || '';
|
|
3117
|
-
|
|
3118
|
-
if (serverName) {
|
|
3119
|
-
console.log(`[${connectionId}] Extracted SNI domain: ${serverName}`);
|
|
3120
|
-
} else {
|
|
3121
|
-
console.log(`[${connectionId}] No SNI found in TLS handshake`);
|
|
3122
|
-
}
|
|
3123
|
-
}
|
|
3124
|
-
} else {
|
|
3125
|
-
console.log(`[${connectionId}] Non-TLS connection detected`);
|
|
1565
|
+
serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
|
|
3126
1566
|
}
|
|
3127
1567
|
|
|
3128
1568
|
// Lock the connection to the negotiated SNI.
|
|
@@ -3221,11 +1661,11 @@ export class PortProxy {
|
|
|
3221
1661
|
} else {
|
|
3222
1662
|
nonTlsConnections++;
|
|
3223
1663
|
}
|
|
3224
|
-
|
|
1664
|
+
|
|
3225
1665
|
if (record.hasKeepAlive) {
|
|
3226
1666
|
keepAliveConnections++;
|
|
3227
1667
|
}
|
|
3228
|
-
|
|
1668
|
+
|
|
3229
1669
|
if (record.usingNetworkProxy) {
|
|
3230
1670
|
networkProxyConnections++;
|
|
3231
1671
|
}
|
|
@@ -3266,80 +1706,35 @@ export class PortProxy {
|
|
|
3266
1706
|
}
|
|
3267
1707
|
|
|
3268
1708
|
// Skip inactivity check if disabled or for immortal keep-alive connections
|
|
3269
|
-
if (
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
) {
|
|
1709
|
+
if (!this.settings.disableInactivityCheck &&
|
|
1710
|
+
!(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')) {
|
|
1711
|
+
|
|
3273
1712
|
const inactivityTime = now - record.lastActivity;
|
|
3274
|
-
|
|
3275
|
-
// Special handling for TLS keep-alive connections
|
|
3276
|
-
if (
|
|
3277
|
-
record.hasKeepAlive &&
|
|
3278
|
-
record.isTLS &&
|
|
3279
|
-
inactivityTime > this.settings.inactivityTimeout! / 2
|
|
3280
|
-
) {
|
|
3281
|
-
// For TLS keep-alive connections that are getting stale, try to refresh before closing
|
|
3282
|
-
if (!record.inactivityWarningIssued) {
|
|
3283
|
-
console.log(
|
|
3284
|
-
`[${id}] TLS keep-alive connection from ${
|
|
3285
|
-
record.remoteIP
|
|
3286
|
-
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
3287
|
-
`Attempting to preserve connection.`
|
|
3288
|
-
);
|
|
3289
|
-
|
|
3290
|
-
// Set warning flag but give a much longer grace period for TLS connections
|
|
3291
|
-
record.inactivityWarningIssued = true;
|
|
3292
|
-
|
|
3293
|
-
// For TLS connections, extend the last activity time considerably
|
|
3294
|
-
// This gives browsers more time to re-establish the connection properly
|
|
3295
|
-
record.lastActivity = now - this.settings.inactivityTimeout! / 3;
|
|
3296
|
-
|
|
3297
|
-
// Try to stimulate the connection with a probe packet
|
|
3298
|
-
if (record.outgoing && !record.outgoing.destroyed) {
|
|
3299
|
-
try {
|
|
3300
|
-
// For TLS connections, send a proper TLS heartbeat-like packet
|
|
3301
|
-
// This is just a small empty buffer that won't affect the TLS session
|
|
3302
|
-
record.outgoing.write(Buffer.alloc(0));
|
|
3303
|
-
|
|
3304
|
-
if (this.settings.enableDetailedLogging) {
|
|
3305
|
-
console.log(`[${id}] Sent TLS keep-alive probe packet`);
|
|
3306
|
-
}
|
|
3307
|
-
} catch (err) {
|
|
3308
|
-
console.log(`[${id}] Error sending TLS probe packet: ${err}`);
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
// Don't proceed to the normal inactivity check logic
|
|
3313
|
-
continue;
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
|
|
1713
|
+
|
|
3317
1714
|
// Use extended timeout for extended-treatment keep-alive connections
|
|
3318
1715
|
let effectiveTimeout = this.settings.inactivityTimeout!;
|
|
3319
1716
|
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
3320
1717
|
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
|
|
3321
1718
|
effectiveTimeout = effectiveTimeout * multiplier;
|
|
3322
1719
|
}
|
|
3323
|
-
|
|
1720
|
+
|
|
3324
1721
|
if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
|
|
3325
1722
|
// For keep-alive connections, issue a warning first
|
|
3326
1723
|
if (record.hasKeepAlive && !record.inactivityWarningIssued) {
|
|
3327
1724
|
console.log(
|
|
3328
|
-
`[${id}] Warning: Keep-alive connection from ${
|
|
3329
|
-
|
|
3330
|
-
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
3331
|
-
`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.`
|
|
3332
1727
|
);
|
|
3333
|
-
|
|
1728
|
+
|
|
3334
1729
|
// Set warning flag and add grace period
|
|
3335
1730
|
record.inactivityWarningIssued = true;
|
|
3336
1731
|
record.lastActivity = now - (effectiveTimeout - 600000);
|
|
3337
|
-
|
|
1732
|
+
|
|
3338
1733
|
// Try to stimulate activity with a probe packet
|
|
3339
1734
|
if (record.outgoing && !record.outgoing.destroyed) {
|
|
3340
1735
|
try {
|
|
3341
1736
|
record.outgoing.write(Buffer.alloc(0));
|
|
3342
|
-
|
|
1737
|
+
|
|
3343
1738
|
if (this.settings.enableDetailedLogging) {
|
|
3344
1739
|
console.log(`[${id}] Sent probe packet to test keep-alive connection`);
|
|
3345
1740
|
}
|
|
@@ -3348,48 +1743,18 @@ export class PortProxy {
|
|
|
3348
1743
|
}
|
|
3349
1744
|
}
|
|
3350
1745
|
} else {
|
|
3351
|
-
//
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
console.log(
|
|
3359
|
-
`[${id}] TLS keep-alive connection from ${
|
|
3360
|
-
record.remoteIP
|
|
3361
|
-
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
3362
|
-
`Closing to ensure proper certificate handling on browser reconnect.`
|
|
3363
|
-
);
|
|
3364
|
-
this.cleanupConnection(record, 'tls_certificate_refresh');
|
|
3365
|
-
} else {
|
|
3366
|
-
// For shorter inactivity periods, add grace period
|
|
3367
|
-
console.log(
|
|
3368
|
-
`[${id}] TLS keep-alive connection from ${
|
|
3369
|
-
record.remoteIP
|
|
3370
|
-
} inactive for ${plugins.prettyMs(inactivityTime)}. ` +
|
|
3371
|
-
`Adding extra grace period.`
|
|
3372
|
-
);
|
|
3373
|
-
|
|
3374
|
-
// Give additional time for browsers to reconnect properly
|
|
3375
|
-
record.lastActivity = now - effectiveTimeout / 2;
|
|
3376
|
-
}
|
|
3377
|
-
} else {
|
|
3378
|
-
// For non-keep-alive or after warning, close the connection
|
|
3379
|
-
console.log(
|
|
3380
|
-
`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
|
|
3381
|
-
`for ${plugins.prettyMs(inactivityTime)}.` +
|
|
3382
|
-
(record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
|
|
3383
|
-
);
|
|
3384
|
-
this.cleanupConnection(record, 'inactivity');
|
|
3385
|
-
}
|
|
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');
|
|
3386
1753
|
}
|
|
3387
1754
|
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
|
|
3388
1755
|
// If activity detected after warning, clear the warning
|
|
3389
1756
|
if (this.settings.enableDetailedLogging) {
|
|
3390
|
-
console.log(
|
|
3391
|
-
`[${id}] Connection activity detected after inactivity warning, resetting warning`
|
|
3392
|
-
);
|
|
1757
|
+
console.log(`[${id}] Connection activity detected after inactivity warning, resetting warning`);
|
|
3393
1758
|
}
|
|
3394
1759
|
record.inactivityWarningIssued = false;
|
|
3395
1760
|
}
|
|
@@ -3438,9 +1803,6 @@ export class PortProxy {
|
|
|
3438
1803
|
public async stop() {
|
|
3439
1804
|
console.log('PortProxy shutting down...');
|
|
3440
1805
|
this.isShuttingDown = true;
|
|
3441
|
-
|
|
3442
|
-
// Stop the session cleanup timer
|
|
3443
|
-
stopSessionCleanupTimer();
|
|
3444
1806
|
|
|
3445
1807
|
// Stop accepting new connections
|
|
3446
1808
|
const closeServerPromises: Promise<void>[] = this.netServers.map(
|
|
@@ -3541,4 +1903,4 @@ export class PortProxy {
|
|
|
3541
1903
|
|
|
3542
1904
|
console.log('PortProxy shutdown complete.');
|
|
3543
1905
|
}
|
|
3544
|
-
}
|
|
1906
|
+
}
|