@push.rocks/smartproxy 3.23.1 → 3.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.networkproxy.d.ts +90 -8
- package/dist_ts/classes.networkproxy.js +605 -221
- package/dist_ts/classes.portproxy.d.ts +66 -1
- package/dist_ts/classes.portproxy.js +620 -109
- package/package.json +8 -8
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.networkproxy.ts +743 -268
- package/ts/classes.portproxy.ts +752 -134
package/ts/classes.portproxy.ts
CHANGED
|
@@ -7,8 +7,14 @@ export interface IDomainConfig {
|
|
|
7
7
|
blockedIPs?: string[]; // Glob patterns for blocked IPs
|
|
8
8
|
targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
|
|
9
9
|
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
10
|
+
// Protocol-specific timeout overrides
|
|
11
|
+
httpTimeout?: number; // HTTP connection timeout override (ms)
|
|
12
|
+
wsTimeout?: number; // WebSocket connection timeout override (ms)
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
/** Connection protocol types for timeout management */
|
|
16
|
+
export type ProtocolType = 'http' | 'websocket' | 'https' | 'tls' | 'unknown';
|
|
17
|
+
|
|
12
18
|
/** Port proxy settings including global allowed port ranges */
|
|
13
19
|
export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
14
20
|
fromPort: number;
|
|
@@ -19,10 +25,66 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
19
25
|
defaultAllowedIPs?: string[];
|
|
20
26
|
defaultBlockedIPs?: string[];
|
|
21
27
|
preserveSourceIP?: boolean;
|
|
22
|
-
|
|
28
|
+
|
|
29
|
+
// Updated timeout settings with better defaults
|
|
30
|
+
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 15000 (15s)
|
|
31
|
+
socketTimeout?: number; // Socket inactivity timeout (ms), default: 300000 (5m)
|
|
32
|
+
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 30000 (30s)
|
|
33
|
+
|
|
34
|
+
// Protocol-specific timeouts
|
|
35
|
+
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
|
|
36
|
+
httpConnectionTimeout?: number; // HTTP specific timeout (ms), default: 1800000 (30m)
|
|
37
|
+
wsConnectionTimeout?: number; // WebSocket specific timeout (ms), default: 14400000 (4h)
|
|
38
|
+
httpKeepAliveTimeout?: number; // HTTP keep-alive header timeout (ms), default: 1200000 (20m)
|
|
39
|
+
|
|
40
|
+
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
|
|
23
41
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
24
|
-
forwardAllGlobalRanges?: boolean;
|
|
25
|
-
|
|
42
|
+
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
|
|
43
|
+
|
|
44
|
+
// Socket optimization settings
|
|
45
|
+
noDelay?: boolean; // Disable Nagle's algorithm (default: true)
|
|
46
|
+
keepAlive?: boolean; // Enable TCP keepalive (default: true)
|
|
47
|
+
keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
|
|
48
|
+
maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
|
|
49
|
+
|
|
50
|
+
// Enable enhanced features
|
|
51
|
+
disableInactivityCheck?: boolean; // Disable inactivity checking entirely
|
|
52
|
+
enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
|
|
53
|
+
enableProtocolDetection?: boolean; // Enable HTTP/WebSocket protocol detection
|
|
54
|
+
enableDetailedLogging?: boolean; // Enable detailed connection logging
|
|
55
|
+
|
|
56
|
+
// Rate limiting and security
|
|
57
|
+
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
|
58
|
+
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Enhanced connection record with protocol-specific handling
|
|
63
|
+
*/
|
|
64
|
+
interface IConnectionRecord {
|
|
65
|
+
id: string; // Unique connection identifier
|
|
66
|
+
incoming: plugins.net.Socket;
|
|
67
|
+
outgoing: plugins.net.Socket | null;
|
|
68
|
+
incomingStartTime: number;
|
|
69
|
+
outgoingStartTime?: number;
|
|
70
|
+
outgoingClosedTime?: number;
|
|
71
|
+
lockedDomain?: string; // Used to lock this connection to the initial SNI
|
|
72
|
+
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
|
|
73
|
+
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
|
|
74
|
+
lastActivity: number; // Last activity timestamp for inactivity detection
|
|
75
|
+
pendingData: Buffer[]; // Buffer to hold data during connection setup
|
|
76
|
+
pendingDataSize: number; // Track total size of pending data
|
|
77
|
+
|
|
78
|
+
// Enhanced tracking fields
|
|
79
|
+
protocolType: ProtocolType; // Connection protocol type
|
|
80
|
+
isPooledConnection: boolean; // Whether this is likely a browser pooled connection
|
|
81
|
+
lastHttpRequest?: number; // Timestamp of last HTTP request (for keep-alive tracking)
|
|
82
|
+
httpKeepAliveTimeout?: number; // HTTP keep-alive timeout from headers
|
|
83
|
+
bytesReceived: number; // Total bytes received
|
|
84
|
+
bytesSent: number; // Total bytes sent
|
|
85
|
+
remoteIP: string; // Remote IP (cached for logging after socket close)
|
|
86
|
+
localPort: number; // Local port (cached for logging)
|
|
87
|
+
httpRequests: number; // Count of HTTP requests on this connection
|
|
26
88
|
}
|
|
27
89
|
|
|
28
90
|
/**
|
|
@@ -88,20 +150,6 @@ function extractSNI(buffer: Buffer): string | undefined {
|
|
|
88
150
|
return undefined;
|
|
89
151
|
}
|
|
90
152
|
|
|
91
|
-
interface IConnectionRecord {
|
|
92
|
-
id: string; // Unique connection identifier
|
|
93
|
-
incoming: plugins.net.Socket;
|
|
94
|
-
outgoing: plugins.net.Socket | null;
|
|
95
|
-
incomingStartTime: number;
|
|
96
|
-
outgoingStartTime?: number;
|
|
97
|
-
outgoingClosedTime?: number;
|
|
98
|
-
lockedDomain?: string; // Used to lock this connection to the initial SNI
|
|
99
|
-
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
|
|
100
|
-
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
|
|
101
|
-
lastActivity: number; // Last activity timestamp for inactivity detection
|
|
102
|
-
pendingData: Buffer[]; // Buffer to hold data during connection setup
|
|
103
|
-
}
|
|
104
|
-
|
|
105
153
|
// Helper: Check if a port falls within any of the given port ranges
|
|
106
154
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
|
107
155
|
return ranges.some(range => port >= range.from && port <= range.to);
|
|
@@ -137,6 +185,34 @@ const generateConnectionId = (): string => {
|
|
|
137
185
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
138
186
|
};
|
|
139
187
|
|
|
188
|
+
// Protocol detection helpers
|
|
189
|
+
const isHttpRequest = (buffer: Buffer): boolean => {
|
|
190
|
+
if (buffer.length < 4) return false;
|
|
191
|
+
const start = buffer.toString('ascii', 0, 4).toUpperCase();
|
|
192
|
+
return (
|
|
193
|
+
start.startsWith('GET ') ||
|
|
194
|
+
start.startsWith('POST') ||
|
|
195
|
+
start.startsWith('PUT ') ||
|
|
196
|
+
start.startsWith('HEAD') ||
|
|
197
|
+
start.startsWith('DELE') ||
|
|
198
|
+
start.startsWith('PATC') ||
|
|
199
|
+
start.startsWith('OPTI')
|
|
200
|
+
);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const isWebSocketUpgrade = (buffer: Buffer): boolean => {
|
|
204
|
+
if (buffer.length < 20) return false;
|
|
205
|
+
const data = buffer.toString('ascii', 0, Math.min(buffer.length, 200));
|
|
206
|
+
return (
|
|
207
|
+
data.includes('Upgrade: websocket') ||
|
|
208
|
+
data.includes('Upgrade: WebSocket')
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const isTlsHandshake = (buffer: Buffer): boolean => {
|
|
213
|
+
return buffer.length > 0 && buffer[0] === 22; // ContentType.handshake
|
|
214
|
+
};
|
|
215
|
+
|
|
140
216
|
export class PortProxy {
|
|
141
217
|
private netServers: plugins.net.Server[] = [];
|
|
142
218
|
settings: IPortProxySettings;
|
|
@@ -147,6 +223,7 @@ export class PortProxy {
|
|
|
147
223
|
// Map to track round robin indices for each domain config
|
|
148
224
|
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
|
|
149
225
|
|
|
226
|
+
// Enhanced stats tracking
|
|
150
227
|
private terminationStats: {
|
|
151
228
|
incoming: Record<string, number>;
|
|
152
229
|
outgoing: Record<string, number>;
|
|
@@ -154,20 +231,218 @@ export class PortProxy {
|
|
|
154
231
|
incoming: {},
|
|
155
232
|
outgoing: {},
|
|
156
233
|
};
|
|
234
|
+
|
|
235
|
+
// Connection tracking by IP for rate limiting
|
|
236
|
+
private connectionsByIP: Map<string, Set<string>> = new Map();
|
|
237
|
+
private connectionRateByIP: Map<string, number[]> = new Map();
|
|
157
238
|
|
|
158
239
|
constructor(settingsArg: IPortProxySettings) {
|
|
240
|
+
// Set reasonable defaults for all settings
|
|
159
241
|
this.settings = {
|
|
160
242
|
...settingsArg,
|
|
161
243
|
targetIP: settingsArg.targetIP || 'localhost',
|
|
162
|
-
|
|
163
|
-
|
|
244
|
+
|
|
245
|
+
// Timeout settings with browser-friendly defaults
|
|
246
|
+
initialDataTimeout: settingsArg.initialDataTimeout || 15000, // 15 seconds
|
|
247
|
+
socketTimeout: settingsArg.socketTimeout || 300000, // 5 minutes
|
|
248
|
+
inactivityCheckInterval: settingsArg.inactivityCheckInterval || 30000, // 30 seconds
|
|
249
|
+
|
|
250
|
+
// Protocol-specific timeouts
|
|
251
|
+
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default
|
|
252
|
+
httpConnectionTimeout: settingsArg.httpConnectionTimeout || 1800000, // 30 minutes
|
|
253
|
+
wsConnectionTimeout: settingsArg.wsConnectionTimeout || 14400000, // 4 hours
|
|
254
|
+
httpKeepAliveTimeout: settingsArg.httpKeepAliveTimeout || 1200000, // 20 minutes
|
|
255
|
+
|
|
256
|
+
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
|
|
257
|
+
|
|
258
|
+
// Socket optimization settings
|
|
259
|
+
noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
|
|
260
|
+
keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
|
|
261
|
+
keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 60000, // 1 minute
|
|
262
|
+
maxPendingDataSize: settingsArg.maxPendingDataSize || 1024 * 1024, // 1MB
|
|
263
|
+
|
|
264
|
+
// Feature flags
|
|
265
|
+
disableInactivityCheck: settingsArg.disableInactivityCheck || false,
|
|
266
|
+
enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false,
|
|
267
|
+
enableProtocolDetection: settingsArg.enableProtocolDetection !== undefined ? settingsArg.enableProtocolDetection : true,
|
|
268
|
+
enableDetailedLogging: settingsArg.enableDetailedLogging || false,
|
|
269
|
+
|
|
270
|
+
// Rate limiting defaults
|
|
271
|
+
maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
|
|
272
|
+
connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
|
|
164
273
|
};
|
|
165
274
|
}
|
|
166
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Get connections count by IP
|
|
278
|
+
*/
|
|
279
|
+
private getConnectionCountByIP(ip: string): number {
|
|
280
|
+
return this.connectionsByIP.get(ip)?.size || 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Check and update connection rate for an IP
|
|
285
|
+
*/
|
|
286
|
+
private checkConnectionRate(ip: string): boolean {
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const minute = 60 * 1000;
|
|
289
|
+
|
|
290
|
+
if (!this.connectionRateByIP.has(ip)) {
|
|
291
|
+
this.connectionRateByIP.set(ip, [now]);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Get timestamps and filter out entries older than 1 minute
|
|
296
|
+
const timestamps = this.connectionRateByIP.get(ip)!.filter(time => now - time < minute);
|
|
297
|
+
timestamps.push(now);
|
|
298
|
+
this.connectionRateByIP.set(ip, timestamps);
|
|
299
|
+
|
|
300
|
+
// Check if rate exceeds limit
|
|
301
|
+
return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Track connection by IP
|
|
306
|
+
*/
|
|
307
|
+
private trackConnectionByIP(ip: string, connectionId: string): void {
|
|
308
|
+
if (!this.connectionsByIP.has(ip)) {
|
|
309
|
+
this.connectionsByIP.set(ip, new Set());
|
|
310
|
+
}
|
|
311
|
+
this.connectionsByIP.get(ip)!.add(connectionId);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Remove connection tracking for an IP
|
|
316
|
+
*/
|
|
317
|
+
private removeConnectionByIP(ip: string, connectionId: string): void {
|
|
318
|
+
if (this.connectionsByIP.has(ip)) {
|
|
319
|
+
const connections = this.connectionsByIP.get(ip)!;
|
|
320
|
+
connections.delete(connectionId);
|
|
321
|
+
if (connections.size === 0) {
|
|
322
|
+
this.connectionsByIP.delete(ip);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Track connection termination statistic
|
|
329
|
+
*/
|
|
167
330
|
private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
|
|
168
331
|
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
|
|
169
332
|
}
|
|
170
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Get protocol-specific timeout based on connection type
|
|
336
|
+
*/
|
|
337
|
+
private getProtocolTimeout(record: IConnectionRecord, domainConfig?: IDomainConfig): number {
|
|
338
|
+
// If the protocol has a domain-specific timeout, use that
|
|
339
|
+
if (domainConfig) {
|
|
340
|
+
if (record.protocolType === 'http' && domainConfig.httpTimeout) {
|
|
341
|
+
return domainConfig.httpTimeout;
|
|
342
|
+
}
|
|
343
|
+
if (record.protocolType === 'websocket' && domainConfig.wsTimeout) {
|
|
344
|
+
return domainConfig.wsTimeout;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Use HTTP keep-alive timeout from headers if available
|
|
349
|
+
if (record.httpKeepAliveTimeout) {
|
|
350
|
+
return record.httpKeepAliveTimeout;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Otherwise use default protocol-specific timeout
|
|
354
|
+
switch (record.protocolType) {
|
|
355
|
+
case 'http':
|
|
356
|
+
return this.settings.httpConnectionTimeout!;
|
|
357
|
+
case 'websocket':
|
|
358
|
+
return this.settings.wsConnectionTimeout!;
|
|
359
|
+
case 'https':
|
|
360
|
+
case 'tls':
|
|
361
|
+
return this.settings.httpConnectionTimeout!; // Use HTTP timeout for HTTPS by default
|
|
362
|
+
default:
|
|
363
|
+
return this.settings.maxConnectionLifetime!;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Detect protocol and update connection record
|
|
369
|
+
*/
|
|
370
|
+
private detectProtocol(data: Buffer, record: IConnectionRecord): void {
|
|
371
|
+
if (!this.settings.enableProtocolDetection || record.protocolType !== 'unknown') {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
// Detect TLS/HTTPS
|
|
377
|
+
if (isTlsHandshake(data)) {
|
|
378
|
+
record.protocolType = 'tls';
|
|
379
|
+
if (this.settings.enableDetailedLogging) {
|
|
380
|
+
console.log(`[${record.id}] Protocol detected: TLS`);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Detect HTTP including WebSocket upgrades
|
|
386
|
+
if (isHttpRequest(data)) {
|
|
387
|
+
record.httpRequests++;
|
|
388
|
+
record.lastHttpRequest = Date.now();
|
|
389
|
+
|
|
390
|
+
// Check for WebSocket upgrade
|
|
391
|
+
if (isWebSocketUpgrade(data)) {
|
|
392
|
+
record.protocolType = 'websocket';
|
|
393
|
+
if (this.settings.enableDetailedLogging) {
|
|
394
|
+
console.log(`[${record.id}] Protocol detected: WebSocket Upgrade`);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
record.protocolType = 'http';
|
|
398
|
+
|
|
399
|
+
// Parse HTTP keep-alive headers
|
|
400
|
+
this.parseHttpHeaders(data, record);
|
|
401
|
+
|
|
402
|
+
if (this.settings.enableDetailedLogging) {
|
|
403
|
+
console.log(`[${record.id}] Protocol detected: HTTP${record.isPooledConnection ? ' (KeepAlive)' : ''}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
console.log(`[${record.id}] Error detecting protocol: ${err}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Parse HTTP headers for keep-alive and other connection info
|
|
414
|
+
*/
|
|
415
|
+
private parseHttpHeaders(data: Buffer, record: IConnectionRecord): void {
|
|
416
|
+
try {
|
|
417
|
+
const headerStr = data.toString('utf8', 0, Math.min(data.length, 1024));
|
|
418
|
+
|
|
419
|
+
// Check for HTTP keep-alive
|
|
420
|
+
const connectionHeader = headerStr.match(/\r\nConnection:\s*([^\r\n]+)/i);
|
|
421
|
+
if (connectionHeader && connectionHeader[1].toLowerCase().includes('keep-alive')) {
|
|
422
|
+
record.isPooledConnection = true;
|
|
423
|
+
|
|
424
|
+
// Check for Keep-Alive timeout value
|
|
425
|
+
const keepAliveHeader = headerStr.match(/\r\nKeep-Alive:\s*([^\r\n]+)/i);
|
|
426
|
+
if (keepAliveHeader) {
|
|
427
|
+
const timeoutMatch = keepAliveHeader[1].match(/timeout=(\d+)/i);
|
|
428
|
+
if (timeoutMatch && timeoutMatch[1]) {
|
|
429
|
+
const timeoutSec = parseInt(timeoutMatch[1], 10);
|
|
430
|
+
if (!isNaN(timeoutSec) && timeoutSec > 0) {
|
|
431
|
+
// Convert seconds to milliseconds and add some buffer
|
|
432
|
+
record.httpKeepAliveTimeout = (timeoutSec * 1000) + 5000;
|
|
433
|
+
|
|
434
|
+
if (this.settings.enableDetailedLogging) {
|
|
435
|
+
console.log(`[${record.id}] HTTP Keep-Alive timeout set to ${timeoutSec} seconds`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
} catch (err) {
|
|
442
|
+
console.log(`[${record.id}] Error parsing HTTP headers: ${err}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
171
446
|
/**
|
|
172
447
|
* Cleans up a connection record.
|
|
173
448
|
* Destroys both incoming and outgoing sockets, clears timers, and removes the record.
|
|
@@ -178,25 +453,47 @@ export class PortProxy {
|
|
|
178
453
|
if (!record.connectionClosed) {
|
|
179
454
|
record.connectionClosed = true;
|
|
180
455
|
|
|
456
|
+
// Track connection termination
|
|
457
|
+
this.removeConnectionByIP(record.remoteIP, record.id);
|
|
458
|
+
|
|
181
459
|
if (record.cleanupTimer) {
|
|
182
460
|
clearTimeout(record.cleanupTimer);
|
|
183
461
|
record.cleanupTimer = undefined;
|
|
184
462
|
}
|
|
185
463
|
|
|
464
|
+
// Detailed logging data
|
|
465
|
+
const duration = Date.now() - record.incomingStartTime;
|
|
466
|
+
const bytesReceived = record.bytesReceived;
|
|
467
|
+
const bytesSent = record.bytesSent;
|
|
468
|
+
const httpRequests = record.httpRequests;
|
|
469
|
+
|
|
186
470
|
try {
|
|
187
471
|
if (!record.incoming.destroyed) {
|
|
188
472
|
// Try graceful shutdown first, then force destroy after a short timeout
|
|
189
473
|
record.incoming.end();
|
|
190
|
-
setTimeout(() => {
|
|
191
|
-
|
|
192
|
-
record.incoming.
|
|
474
|
+
const incomingTimeout = setTimeout(() => {
|
|
475
|
+
try {
|
|
476
|
+
if (record && !record.incoming.destroyed) {
|
|
477
|
+
record.incoming.destroy();
|
|
478
|
+
}
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.log(`[${record.id}] Error destroying incoming socket: ${err}`);
|
|
193
481
|
}
|
|
194
482
|
}, 1000);
|
|
483
|
+
|
|
484
|
+
// Ensure the timeout doesn't block Node from exiting
|
|
485
|
+
if (incomingTimeout.unref) {
|
|
486
|
+
incomingTimeout.unref();
|
|
487
|
+
}
|
|
195
488
|
}
|
|
196
489
|
} catch (err) {
|
|
197
|
-
console.log(`Error closing incoming socket: ${err}`);
|
|
198
|
-
|
|
199
|
-
record.incoming.
|
|
490
|
+
console.log(`[${record.id}] Error closing incoming socket: ${err}`);
|
|
491
|
+
try {
|
|
492
|
+
if (!record.incoming.destroyed) {
|
|
493
|
+
record.incoming.destroy();
|
|
494
|
+
}
|
|
495
|
+
} catch (destroyErr) {
|
|
496
|
+
console.log(`[${record.id}] Error destroying incoming socket: ${destroyErr}`);
|
|
200
497
|
}
|
|
201
498
|
}
|
|
202
499
|
|
|
@@ -204,31 +501,60 @@ export class PortProxy {
|
|
|
204
501
|
if (record.outgoing && !record.outgoing.destroyed) {
|
|
205
502
|
// Try graceful shutdown first, then force destroy after a short timeout
|
|
206
503
|
record.outgoing.end();
|
|
207
|
-
setTimeout(() => {
|
|
208
|
-
|
|
209
|
-
record.outgoing.
|
|
504
|
+
const outgoingTimeout = setTimeout(() => {
|
|
505
|
+
try {
|
|
506
|
+
if (record && record.outgoing && !record.outgoing.destroyed) {
|
|
507
|
+
record.outgoing.destroy();
|
|
508
|
+
}
|
|
509
|
+
} catch (err) {
|
|
510
|
+
console.log(`[${record.id}] Error destroying outgoing socket: ${err}`);
|
|
210
511
|
}
|
|
211
512
|
}, 1000);
|
|
513
|
+
|
|
514
|
+
// Ensure the timeout doesn't block Node from exiting
|
|
515
|
+
if (outgoingTimeout.unref) {
|
|
516
|
+
outgoingTimeout.unref();
|
|
517
|
+
}
|
|
212
518
|
}
|
|
213
519
|
} catch (err) {
|
|
214
|
-
console.log(`Error closing outgoing socket: ${err}`);
|
|
215
|
-
|
|
216
|
-
record.outgoing.
|
|
520
|
+
console.log(`[${record.id}] Error closing outgoing socket: ${err}`);
|
|
521
|
+
try {
|
|
522
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
523
|
+
record.outgoing.destroy();
|
|
524
|
+
}
|
|
525
|
+
} catch (destroyErr) {
|
|
526
|
+
console.log(`[${record.id}] Error destroying outgoing socket: ${destroyErr}`);
|
|
217
527
|
}
|
|
218
528
|
}
|
|
219
529
|
|
|
530
|
+
// Clear pendingData to avoid memory leaks
|
|
531
|
+
record.pendingData = [];
|
|
532
|
+
record.pendingDataSize = 0;
|
|
533
|
+
|
|
220
534
|
// Remove the record from the tracking map
|
|
221
535
|
this.connectionRecords.delete(record.id);
|
|
222
536
|
|
|
223
|
-
|
|
224
|
-
|
|
537
|
+
// Log connection details
|
|
538
|
+
if (this.settings.enableDetailedLogging) {
|
|
539
|
+
console.log(`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
|
|
540
|
+
` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
541
|
+
`HTTP Requests: ${httpRequests}, Protocol: ${record.protocolType}, Pooled: ${record.isPooledConnection}`);
|
|
542
|
+
} else {
|
|
543
|
+
console.log(`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`);
|
|
544
|
+
}
|
|
225
545
|
}
|
|
226
546
|
}
|
|
227
547
|
|
|
548
|
+
/**
|
|
549
|
+
* Update connection activity timestamp
|
|
550
|
+
*/
|
|
228
551
|
private updateActivity(record: IConnectionRecord): void {
|
|
229
552
|
record.lastActivity = Date.now();
|
|
230
553
|
}
|
|
231
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Get target IP with round-robin support
|
|
557
|
+
*/
|
|
232
558
|
private getTargetIP(domainConfig: IDomainConfig): string {
|
|
233
559
|
if (domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
|
|
234
560
|
const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
|
|
@@ -239,7 +565,16 @@ export class PortProxy {
|
|
|
239
565
|
return this.settings.targetIP!;
|
|
240
566
|
}
|
|
241
567
|
|
|
568
|
+
/**
|
|
569
|
+
* Main method to start the proxy
|
|
570
|
+
*/
|
|
242
571
|
public async start() {
|
|
572
|
+
// Don't start if already shutting down
|
|
573
|
+
if (this.isShuttingDown) {
|
|
574
|
+
console.log("Cannot start PortProxy while it's shutting down");
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
|
|
243
578
|
// Define a unified connection handler for all listening ports.
|
|
244
579
|
const connectionHandler = (socket: plugins.net.Socket) => {
|
|
245
580
|
if (this.isShuttingDown) {
|
|
@@ -249,8 +584,31 @@ export class PortProxy {
|
|
|
249
584
|
}
|
|
250
585
|
|
|
251
586
|
const remoteIP = socket.remoteAddress || '';
|
|
252
|
-
const localPort = socket.localPort; // The port on which this connection was accepted.
|
|
587
|
+
const localPort = socket.localPort || 0; // The port on which this connection was accepted.
|
|
588
|
+
|
|
589
|
+
// Check rate limits
|
|
590
|
+
if (this.settings.maxConnectionsPerIP &&
|
|
591
|
+
this.getConnectionCountByIP(remoteIP) >= this.settings.maxConnectionsPerIP) {
|
|
592
|
+
console.log(`Connection rejected from ${remoteIP}: Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`);
|
|
593
|
+
socket.end();
|
|
594
|
+
socket.destroy();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (this.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(remoteIP)) {
|
|
599
|
+
console.log(`Connection rejected from ${remoteIP}: Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`);
|
|
600
|
+
socket.end();
|
|
601
|
+
socket.destroy();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
253
604
|
|
|
605
|
+
// Apply socket optimizations
|
|
606
|
+
socket.setNoDelay(this.settings.noDelay);
|
|
607
|
+
if (this.settings.keepAlive) {
|
|
608
|
+
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Create a unique connection ID and record
|
|
254
612
|
const connectionId = generateConnectionId();
|
|
255
613
|
const connectionRecord: IConnectionRecord = {
|
|
256
614
|
id: connectionId,
|
|
@@ -259,11 +617,28 @@ export class PortProxy {
|
|
|
259
617
|
incomingStartTime: Date.now(),
|
|
260
618
|
lastActivity: Date.now(),
|
|
261
619
|
connectionClosed: false,
|
|
262
|
-
pendingData: []
|
|
620
|
+
pendingData: [],
|
|
621
|
+
pendingDataSize: 0,
|
|
622
|
+
|
|
623
|
+
// Initialize enhanced tracking fields
|
|
624
|
+
protocolType: 'unknown',
|
|
625
|
+
isPooledConnection: false,
|
|
626
|
+
bytesReceived: 0,
|
|
627
|
+
bytesSent: 0,
|
|
628
|
+
remoteIP: remoteIP,
|
|
629
|
+
localPort: localPort,
|
|
630
|
+
httpRequests: 0
|
|
263
631
|
};
|
|
632
|
+
|
|
633
|
+
// Track connection by IP
|
|
634
|
+
this.trackConnectionByIP(remoteIP, connectionId);
|
|
264
635
|
this.connectionRecords.set(connectionId, connectionRecord);
|
|
265
636
|
|
|
266
|
-
|
|
637
|
+
if (this.settings.enableDetailedLogging) {
|
|
638
|
+
console.log(`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
|
|
639
|
+
} else {
|
|
640
|
+
console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
|
|
641
|
+
}
|
|
267
642
|
|
|
268
643
|
let initialDataReceived = false;
|
|
269
644
|
let incomingTerminationReason: string | null = null;
|
|
@@ -274,15 +649,21 @@ export class PortProxy {
|
|
|
274
649
|
this.cleanupConnection(connectionRecord);
|
|
275
650
|
};
|
|
276
651
|
|
|
277
|
-
// Define initiateCleanupOnce for compatibility
|
|
652
|
+
// Define initiateCleanupOnce for compatibility
|
|
278
653
|
const initiateCleanupOnce = (reason: string = 'normal') => {
|
|
279
|
-
|
|
654
|
+
if (this.settings.enableDetailedLogging) {
|
|
655
|
+
console.log(`[${connectionId}] Connection cleanup initiated for ${remoteIP} (${reason})`);
|
|
656
|
+
}
|
|
657
|
+
if (incomingTerminationReason === null) {
|
|
658
|
+
incomingTerminationReason = reason;
|
|
659
|
+
this.incrementTerminationStat('incoming', reason);
|
|
660
|
+
}
|
|
280
661
|
cleanupOnce();
|
|
281
662
|
};
|
|
282
663
|
|
|
283
664
|
// Helper to reject an incoming connection
|
|
284
665
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
|
285
|
-
console.log(logMessage);
|
|
666
|
+
console.log(`[${connectionId}] ${logMessage}`);
|
|
286
667
|
socket.end();
|
|
287
668
|
if (incomingTerminationReason === null) {
|
|
288
669
|
incomingTerminationReason = reason;
|
|
@@ -296,28 +677,83 @@ export class PortProxy {
|
|
|
296
677
|
if (this.settings.sniEnabled) {
|
|
297
678
|
initialTimeout = setTimeout(() => {
|
|
298
679
|
if (!initialDataReceived) {
|
|
299
|
-
console.log(`Initial data timeout for ${remoteIP}`);
|
|
680
|
+
console.log(`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`);
|
|
681
|
+
if (incomingTerminationReason === null) {
|
|
682
|
+
incomingTerminationReason = 'initial_timeout';
|
|
683
|
+
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
684
|
+
}
|
|
300
685
|
socket.end();
|
|
301
686
|
cleanupOnce();
|
|
302
687
|
}
|
|
303
|
-
},
|
|
688
|
+
}, this.settings.initialDataTimeout);
|
|
304
689
|
} else {
|
|
305
690
|
initialDataReceived = true;
|
|
306
691
|
}
|
|
307
692
|
|
|
308
693
|
socket.on('error', (err: Error) => {
|
|
309
|
-
console.log(`Incoming socket error from ${remoteIP}: ${err.message}`);
|
|
694
|
+
console.log(`[${connectionId}] Incoming socket error from ${remoteIP}: ${err.message}`);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
// Track data for bytes counting
|
|
698
|
+
socket.on('data', (chunk: Buffer) => {
|
|
699
|
+
connectionRecord.bytesReceived += chunk.length;
|
|
700
|
+
this.updateActivity(connectionRecord);
|
|
701
|
+
|
|
702
|
+
// Detect protocol on first data chunk
|
|
703
|
+
if (connectionRecord.protocolType === 'unknown') {
|
|
704
|
+
this.detectProtocol(chunk, connectionRecord);
|
|
705
|
+
|
|
706
|
+
// Update timeout based on protocol
|
|
707
|
+
if (connectionRecord.cleanupTimer) {
|
|
708
|
+
clearTimeout(connectionRecord.cleanupTimer);
|
|
709
|
+
|
|
710
|
+
// Set new timeout based on protocol
|
|
711
|
+
const protocolTimeout = this.getProtocolTimeout(connectionRecord);
|
|
712
|
+
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
713
|
+
console.log(`[${connectionId}] ${connectionRecord.protocolType} connection timeout after ${plugins.prettyMs(protocolTimeout)}`);
|
|
714
|
+
initiateCleanupOnce(`${connectionRecord.protocolType}_timeout`);
|
|
715
|
+
}, protocolTimeout);
|
|
716
|
+
}
|
|
717
|
+
} else if (connectionRecord.protocolType === 'http' && isHttpRequest(chunk)) {
|
|
718
|
+
// Additional HTTP request on the same connection
|
|
719
|
+
connectionRecord.httpRequests++;
|
|
720
|
+
connectionRecord.lastHttpRequest = Date.now();
|
|
721
|
+
|
|
722
|
+
// Parse HTTP headers again for keep-alive changes
|
|
723
|
+
this.parseHttpHeaders(chunk, connectionRecord);
|
|
724
|
+
|
|
725
|
+
// Update timeout based on new HTTP headers
|
|
726
|
+
if (connectionRecord.cleanupTimer) {
|
|
727
|
+
clearTimeout(connectionRecord.cleanupTimer);
|
|
728
|
+
|
|
729
|
+
// Set new timeout based on updated HTTP info
|
|
730
|
+
const protocolTimeout = this.getProtocolTimeout(connectionRecord);
|
|
731
|
+
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
732
|
+
console.log(`[${connectionId}] HTTP connection timeout after ${plugins.prettyMs(protocolTimeout)}`);
|
|
733
|
+
initiateCleanupOnce('http_timeout');
|
|
734
|
+
}, protocolTimeout);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
310
737
|
});
|
|
311
738
|
|
|
312
739
|
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
|
313
740
|
const code = (err as any).code;
|
|
314
741
|
let reason = 'error';
|
|
742
|
+
|
|
743
|
+
const now = Date.now();
|
|
744
|
+
const connectionDuration = now - connectionRecord.incomingStartTime;
|
|
745
|
+
const lastActivityAge = now - connectionRecord.lastActivity;
|
|
746
|
+
|
|
315
747
|
if (code === 'ECONNRESET') {
|
|
316
748
|
reason = 'econnreset';
|
|
317
|
-
console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
|
|
749
|
+
console.log(`[${connectionId}] ECONNRESET on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`);
|
|
750
|
+
} else if (code === 'ETIMEDOUT') {
|
|
751
|
+
reason = 'etimedout';
|
|
752
|
+
console.log(`[${connectionId}] ETIMEDOUT on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`);
|
|
318
753
|
} else {
|
|
319
|
-
console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
|
|
754
|
+
console.log(`[${connectionId}] Error on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`);
|
|
320
755
|
}
|
|
756
|
+
|
|
321
757
|
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
322
758
|
incomingTerminationReason = reason;
|
|
323
759
|
this.incrementTerminationStat('incoming', reason);
|
|
@@ -325,11 +761,15 @@ export class PortProxy {
|
|
|
325
761
|
outgoingTerminationReason = reason;
|
|
326
762
|
this.incrementTerminationStat('outgoing', reason);
|
|
327
763
|
}
|
|
764
|
+
|
|
328
765
|
initiateCleanupOnce(reason);
|
|
329
766
|
};
|
|
330
767
|
|
|
331
768
|
const handleClose = (side: 'incoming' | 'outgoing') => () => {
|
|
332
|
-
|
|
769
|
+
if (this.settings.enableDetailedLogging) {
|
|
770
|
+
console.log(`[${connectionId}] Connection closed on ${side} side from ${remoteIP}`);
|
|
771
|
+
}
|
|
772
|
+
|
|
333
773
|
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
334
774
|
incomingTerminationReason = 'normal';
|
|
335
775
|
this.incrementTerminationStat('incoming', 'normal');
|
|
@@ -339,6 +779,7 @@ export class PortProxy {
|
|
|
339
779
|
// Record the time when outgoing socket closed.
|
|
340
780
|
connectionRecord.outgoingClosedTime = Date.now();
|
|
341
781
|
}
|
|
782
|
+
|
|
342
783
|
initiateCleanupOnce('closed_' + side);
|
|
343
784
|
};
|
|
344
785
|
|
|
@@ -356,6 +797,11 @@ export class PortProxy {
|
|
|
356
797
|
initialTimeout = null;
|
|
357
798
|
}
|
|
358
799
|
|
|
800
|
+
// Detect protocol if initial chunk is available
|
|
801
|
+
if (initialChunk && this.settings.enableProtocolDetection) {
|
|
802
|
+
this.detectProtocol(initialChunk, connectionRecord);
|
|
803
|
+
}
|
|
804
|
+
|
|
359
805
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
|
360
806
|
const domainConfig = forcedDomain
|
|
361
807
|
? forcedDomain
|
|
@@ -393,9 +839,31 @@ export class PortProxy {
|
|
|
393
839
|
connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
|
|
394
840
|
}
|
|
395
841
|
|
|
842
|
+
// Pause the incoming socket to prevent buffer overflows
|
|
843
|
+
socket.pause();
|
|
844
|
+
|
|
396
845
|
// Temporary handler to collect data during connection setup
|
|
397
846
|
const tempDataHandler = (chunk: Buffer) => {
|
|
847
|
+
// Track bytes received
|
|
848
|
+
connectionRecord.bytesReceived += chunk.length;
|
|
849
|
+
|
|
850
|
+
// Detect protocol even during connection setup
|
|
851
|
+
if (this.settings.enableProtocolDetection && connectionRecord.protocolType === 'unknown') {
|
|
852
|
+
this.detectProtocol(chunk, connectionRecord);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Check if adding this chunk would exceed the buffer limit
|
|
856
|
+
const newSize = connectionRecord.pendingDataSize + chunk.length;
|
|
857
|
+
|
|
858
|
+
if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
|
|
859
|
+
console.log(`[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`);
|
|
860
|
+
socket.end(); // Gracefully close the socket
|
|
861
|
+
return initiateCleanupOnce('buffer_limit_exceeded');
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Buffer the chunk and update the size counter
|
|
398
865
|
connectionRecord.pendingData.push(Buffer.from(chunk));
|
|
866
|
+
connectionRecord.pendingDataSize = newSize;
|
|
399
867
|
this.updateActivity(connectionRecord);
|
|
400
868
|
};
|
|
401
869
|
|
|
@@ -404,7 +872,9 @@ export class PortProxy {
|
|
|
404
872
|
|
|
405
873
|
// Add initial chunk to pending data if present
|
|
406
874
|
if (initialChunk) {
|
|
875
|
+
connectionRecord.bytesReceived += initialChunk.length;
|
|
407
876
|
connectionRecord.pendingData.push(Buffer.from(initialChunk));
|
|
877
|
+
connectionRecord.pendingDataSize = initialChunk.length;
|
|
408
878
|
}
|
|
409
879
|
|
|
410
880
|
// Create the target socket but don't set up piping immediately
|
|
@@ -412,23 +882,62 @@ export class PortProxy {
|
|
|
412
882
|
connectionRecord.outgoing = targetSocket;
|
|
413
883
|
connectionRecord.outgoingStartTime = Date.now();
|
|
414
884
|
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
885
|
+
// Apply socket optimizations
|
|
886
|
+
targetSocket.setNoDelay(this.settings.noDelay);
|
|
887
|
+
if (this.settings.keepAlive) {
|
|
888
|
+
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Setup specific error handler for connection phase
|
|
892
|
+
targetSocket.once('error', (err) => {
|
|
893
|
+
// This handler runs only once during the initial connection phase
|
|
894
|
+
const code = (err as any).code;
|
|
895
|
+
console.log(`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`);
|
|
896
|
+
|
|
897
|
+
// Resume the incoming socket to prevent it from hanging
|
|
898
|
+
socket.resume();
|
|
899
|
+
|
|
900
|
+
if (code === 'ECONNREFUSED') {
|
|
901
|
+
console.log(`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`);
|
|
902
|
+
} else if (code === 'ETIMEDOUT') {
|
|
903
|
+
console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`);
|
|
904
|
+
} else if (code === 'ECONNRESET') {
|
|
905
|
+
console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`);
|
|
906
|
+
} else if (code === 'EHOSTUNREACH') {
|
|
907
|
+
console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Clear any existing error handler after connection phase
|
|
911
|
+
targetSocket.removeAllListeners('error');
|
|
912
|
+
|
|
913
|
+
// Re-add the normal error handler for established connections
|
|
914
|
+
targetSocket.on('error', handleError('outgoing'));
|
|
915
|
+
|
|
916
|
+
if (outgoingTerminationReason === null) {
|
|
917
|
+
outgoingTerminationReason = 'connection_failed';
|
|
918
|
+
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Clean up the connection
|
|
922
|
+
initiateCleanupOnce(`connection_failed_${code}`);
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// Setup close handler
|
|
419
926
|
targetSocket.on('close', handleClose('outgoing'));
|
|
927
|
+
socket.on('close', handleClose('incoming'));
|
|
420
928
|
|
|
421
929
|
// Handle timeouts
|
|
422
930
|
socket.on('timeout', () => {
|
|
423
|
-
console.log(`Timeout on incoming side from ${remoteIP}`);
|
|
931
|
+
console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 300000)}`);
|
|
424
932
|
if (incomingTerminationReason === null) {
|
|
425
933
|
incomingTerminationReason = 'timeout';
|
|
426
934
|
this.incrementTerminationStat('incoming', 'timeout');
|
|
427
935
|
}
|
|
428
936
|
initiateCleanupOnce('timeout_incoming');
|
|
429
937
|
});
|
|
938
|
+
|
|
430
939
|
targetSocket.on('timeout', () => {
|
|
431
|
-
console.log(`Timeout on outgoing side from ${remoteIP}`);
|
|
940
|
+
console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 300000)}`);
|
|
432
941
|
if (outgoingTerminationReason === null) {
|
|
433
942
|
outgoingTerminationReason = 'timeout';
|
|
434
943
|
this.incrementTerminationStat('outgoing', 'timeout');
|
|
@@ -436,12 +945,24 @@ export class PortProxy {
|
|
|
436
945
|
initiateCleanupOnce('timeout_outgoing');
|
|
437
946
|
});
|
|
438
947
|
|
|
439
|
-
// Set appropriate timeouts
|
|
440
|
-
socket.setTimeout(
|
|
441
|
-
targetSocket.setTimeout(
|
|
948
|
+
// Set appropriate timeouts using the configured value
|
|
949
|
+
socket.setTimeout(this.settings.socketTimeout || 300000);
|
|
950
|
+
targetSocket.setTimeout(this.settings.socketTimeout || 300000);
|
|
951
|
+
|
|
952
|
+
// Track outgoing data for bytes counting
|
|
953
|
+
targetSocket.on('data', (chunk: Buffer) => {
|
|
954
|
+
connectionRecord.bytesSent += chunk.length;
|
|
955
|
+
this.updateActivity(connectionRecord);
|
|
956
|
+
});
|
|
442
957
|
|
|
443
958
|
// Wait for the outgoing connection to be ready before setting up piping
|
|
444
959
|
targetSocket.once('connect', () => {
|
|
960
|
+
// Clear the initial connection error handler
|
|
961
|
+
targetSocket.removeAllListeners('error');
|
|
962
|
+
|
|
963
|
+
// Add the normal error handler for established connections
|
|
964
|
+
targetSocket.on('error', handleError('outgoing'));
|
|
965
|
+
|
|
445
966
|
// Remove temporary data handler
|
|
446
967
|
socket.removeListener('data', tempDataHandler);
|
|
447
968
|
|
|
@@ -450,44 +971,53 @@ export class PortProxy {
|
|
|
450
971
|
const combinedData = Buffer.concat(connectionRecord.pendingData);
|
|
451
972
|
targetSocket.write(combinedData, (err) => {
|
|
452
973
|
if (err) {
|
|
453
|
-
console.log(`Error writing pending data to target: ${err.message}`);
|
|
974
|
+
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
|
|
454
975
|
return initiateCleanupOnce('write_error');
|
|
455
976
|
}
|
|
456
977
|
|
|
457
|
-
// Now set up piping for future data
|
|
978
|
+
// Now set up piping for future data and resume the socket
|
|
458
979
|
socket.pipe(targetSocket);
|
|
459
980
|
targetSocket.pipe(socket);
|
|
981
|
+
socket.resume(); // Resume the socket after piping is established
|
|
460
982
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
983
|
+
if (this.settings.enableDetailedLogging) {
|
|
984
|
+
console.log(
|
|
985
|
+
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
986
|
+
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
|
|
987
|
+
` Protocol: ${connectionRecord.protocolType}`
|
|
988
|
+
);
|
|
989
|
+
} else {
|
|
990
|
+
console.log(
|
|
991
|
+
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
992
|
+
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
465
995
|
});
|
|
466
996
|
} else {
|
|
467
997
|
// No pending data, so just set up piping
|
|
468
998
|
socket.pipe(targetSocket);
|
|
469
999
|
targetSocket.pipe(socket);
|
|
1000
|
+
socket.resume(); // Resume the socket after piping is established
|
|
470
1001
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1002
|
+
if (this.settings.enableDetailedLogging) {
|
|
1003
|
+
console.log(
|
|
1004
|
+
`[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1005
|
+
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
|
|
1006
|
+
` Protocol: ${connectionRecord.protocolType}`
|
|
1007
|
+
);
|
|
1008
|
+
} else {
|
|
1009
|
+
console.log(
|
|
1010
|
+
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
1011
|
+
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
475
1014
|
}
|
|
476
1015
|
|
|
477
1016
|
// Clear the buffer now that we've processed it
|
|
478
1017
|
connectionRecord.pendingData = [];
|
|
1018
|
+
connectionRecord.pendingDataSize = 0;
|
|
479
1019
|
|
|
480
|
-
//
|
|
481
|
-
socket.on('data', () => {
|
|
482
|
-
connectionRecord.lastActivity = Date.now();
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
targetSocket.on('data', () => {
|
|
486
|
-
connectionRecord.lastActivity = Date.now();
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
// Add the renegotiation listener (we don't need setImmediate here anymore
|
|
490
|
-
// since we're already in the connect callback)
|
|
1020
|
+
// Add the renegotiation listener for SNI validation
|
|
491
1021
|
if (serverName) {
|
|
492
1022
|
socket.on('data', (renegChunk: Buffer) => {
|
|
493
1023
|
if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
|
|
@@ -495,26 +1025,30 @@ export class PortProxy {
|
|
|
495
1025
|
// Try to extract SNI from potential renegotiation
|
|
496
1026
|
const newSNI = extractSNI(renegChunk);
|
|
497
1027
|
if (newSNI && newSNI !== connectionRecord.lockedDomain) {
|
|
498
|
-
console.log(`Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
|
|
1028
|
+
console.log(`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
|
|
499
1029
|
initiateCleanupOnce('sni_mismatch');
|
|
500
|
-
} else if (newSNI) {
|
|
501
|
-
console.log(`Rehandshake detected with same SNI: ${newSNI}. Allowing.`);
|
|
1030
|
+
} else if (newSNI && this.settings.enableDetailedLogging) {
|
|
1031
|
+
console.log(`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`);
|
|
502
1032
|
}
|
|
503
1033
|
} catch (err) {
|
|
504
|
-
console.log(`Error processing potential renegotiation: ${err}. Allowing connection to continue.`);
|
|
1034
|
+
console.log(`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`);
|
|
505
1035
|
}
|
|
506
1036
|
}
|
|
507
1037
|
});
|
|
508
1038
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
1039
|
+
|
|
1040
|
+
// Set protocol-specific timeout based on detected protocol
|
|
1041
|
+
if (connectionRecord.cleanupTimer) {
|
|
1042
|
+
clearTimeout(connectionRecord.cleanupTimer);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// Set timeout based on protocol
|
|
1046
|
+
const protocolTimeout = this.getProtocolTimeout(connectionRecord, domainConfig);
|
|
513
1047
|
connectionRecord.cleanupTimer = setTimeout(() => {
|
|
514
|
-
console.log(`
|
|
515
|
-
initiateCleanupOnce(
|
|
516
|
-
},
|
|
517
|
-
}
|
|
1048
|
+
console.log(`[${connectionId}] ${connectionRecord.protocolType} connection exceeded max lifetime (${plugins.prettyMs(protocolTimeout)}), forcing cleanup.`);
|
|
1049
|
+
initiateCleanupOnce(`${connectionRecord.protocolType}_max_lifetime`);
|
|
1050
|
+
}, protocolTimeout);
|
|
1051
|
+
});
|
|
518
1052
|
};
|
|
519
1053
|
|
|
520
1054
|
// --- PORT RANGE-BASED HANDLING ---
|
|
@@ -522,11 +1056,13 @@ export class PortProxy {
|
|
|
522
1056
|
if (this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) {
|
|
523
1057
|
if (this.settings.forwardAllGlobalRanges) {
|
|
524
1058
|
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
525
|
-
console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
|
|
1059
|
+
console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
|
|
526
1060
|
socket.end();
|
|
527
1061
|
return;
|
|
528
1062
|
}
|
|
529
|
-
|
|
1063
|
+
if (this.settings.enableDetailedLogging) {
|
|
1064
|
+
console.log(`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
|
|
1065
|
+
}
|
|
530
1066
|
setupConnection('', undefined, {
|
|
531
1067
|
domains: ['global'],
|
|
532
1068
|
allowedIPs: this.settings.defaultAllowedIPs || [],
|
|
@@ -550,11 +1086,13 @@ export class PortProxy {
|
|
|
550
1086
|
...(this.settings.defaultBlockedIPs || [])
|
|
551
1087
|
];
|
|
552
1088
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
553
|
-
console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
|
|
1089
|
+
console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
|
|
554
1090
|
socket.end();
|
|
555
1091
|
return;
|
|
556
1092
|
}
|
|
557
|
-
|
|
1093
|
+
if (this.settings.enableDetailedLogging) {
|
|
1094
|
+
console.log(`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
|
|
1095
|
+
}
|
|
558
1096
|
setupConnection('', undefined, forcedDomain, localPort);
|
|
559
1097
|
return;
|
|
560
1098
|
}
|
|
@@ -576,7 +1114,10 @@ export class PortProxy {
|
|
|
576
1114
|
const serverName = extractSNI(chunk) || '';
|
|
577
1115
|
// Lock the connection to the negotiated SNI.
|
|
578
1116
|
connectionRecord.lockedDomain = serverName;
|
|
579
|
-
|
|
1117
|
+
|
|
1118
|
+
if (this.settings.enableDetailedLogging) {
|
|
1119
|
+
console.log(`[${connectionId}] Received connection from ${remoteIP} with SNI: ${serverName || '(empty)'}`);
|
|
1120
|
+
}
|
|
580
1121
|
|
|
581
1122
|
setupConnection(serverName, chunk);
|
|
582
1123
|
});
|
|
@@ -618,13 +1159,19 @@ export class PortProxy {
|
|
|
618
1159
|
this.netServers.push(server);
|
|
619
1160
|
}
|
|
620
1161
|
|
|
621
|
-
// Log active connection count, longest running durations, and run parity checks
|
|
1162
|
+
// Log active connection count, longest running durations, and run parity checks periodically
|
|
622
1163
|
this.connectionLogger = setInterval(() => {
|
|
1164
|
+
// Immediately return if shutting down
|
|
623
1165
|
if (this.isShuttingDown) return;
|
|
624
1166
|
|
|
625
1167
|
const now = Date.now();
|
|
626
1168
|
let maxIncoming = 0;
|
|
627
1169
|
let maxOutgoing = 0;
|
|
1170
|
+
let httpConnections = 0;
|
|
1171
|
+
let wsConnections = 0;
|
|
1172
|
+
let tlsConnections = 0;
|
|
1173
|
+
let unknownConnections = 0;
|
|
1174
|
+
let pooledConnections = 0;
|
|
628
1175
|
|
|
629
1176
|
// Create a copy of the keys to avoid modification during iteration
|
|
630
1177
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
@@ -633,6 +1180,19 @@ export class PortProxy {
|
|
|
633
1180
|
const record = this.connectionRecords.get(id);
|
|
634
1181
|
if (!record) continue;
|
|
635
1182
|
|
|
1183
|
+
// Track connection stats by protocol
|
|
1184
|
+
switch (record.protocolType) {
|
|
1185
|
+
case 'http': httpConnections++; break;
|
|
1186
|
+
case 'websocket': wsConnections++; break;
|
|
1187
|
+
case 'tls':
|
|
1188
|
+
case 'https': tlsConnections++; break;
|
|
1189
|
+
default: unknownConnections++; break;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if (record.isPooledConnection) {
|
|
1193
|
+
pooledConnections++;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
636
1196
|
maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
|
|
637
1197
|
if (record.outgoingStartTime) {
|
|
638
1198
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
|
@@ -643,30 +1203,51 @@ export class PortProxy {
|
|
|
643
1203
|
!record.incoming.destroyed &&
|
|
644
1204
|
!record.connectionClosed &&
|
|
645
1205
|
(now - record.outgoingClosedTime > 30000)) {
|
|
646
|
-
const remoteIP = record.
|
|
647
|
-
console.log(`Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`);
|
|
1206
|
+
const remoteIP = record.remoteIP;
|
|
1207
|
+
console.log(`[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`);
|
|
648
1208
|
this.cleanupConnection(record, 'parity_check');
|
|
649
1209
|
}
|
|
650
1210
|
|
|
651
|
-
//
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1211
|
+
// Skip inactivity check if disabled
|
|
1212
|
+
if (!this.settings.disableInactivityCheck) {
|
|
1213
|
+
// Inactivity check - use protocol-specific values
|
|
1214
|
+
let inactivityThreshold = 180000; // 3 minutes default
|
|
1215
|
+
|
|
1216
|
+
// Set protocol-specific inactivity thresholds
|
|
1217
|
+
if (record.protocolType === 'http' && record.isPooledConnection) {
|
|
1218
|
+
inactivityThreshold = this.settings.httpKeepAliveTimeout || 1200000; // 20 minutes for pooled HTTP
|
|
1219
|
+
} else if (record.protocolType === 'websocket') {
|
|
1220
|
+
inactivityThreshold = this.settings.wsConnectionTimeout || 14400000; // 4 hours for WebSocket
|
|
1221
|
+
} else if (record.protocolType === 'http') {
|
|
1222
|
+
inactivityThreshold = this.settings.httpConnectionTimeout || 1800000; // 30 minutes for HTTP
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const inactivityTime = now - record.lastActivity;
|
|
1226
|
+
if (inactivityTime > inactivityThreshold && !record.connectionClosed) {
|
|
1227
|
+
console.log(`[${id}] Inactivity check: No activity on ${record.protocolType} connection from ${record.remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
|
|
1228
|
+
this.cleanupConnection(record, 'inactivity');
|
|
1229
|
+
}
|
|
658
1230
|
}
|
|
659
1231
|
}
|
|
660
1232
|
|
|
1233
|
+
// Log detailed stats periodically
|
|
661
1234
|
console.log(
|
|
662
|
-
`
|
|
663
|
-
`
|
|
664
|
-
`
|
|
665
|
-
`
|
|
1235
|
+
`Active connections: ${this.connectionRecords.size}. ` +
|
|
1236
|
+
`Types: HTTP=${httpConnections}, WS=${wsConnections}, TLS=${tlsConnections}, Unknown=${unknownConnections}, Pooled=${pooledConnections}. ` +
|
|
1237
|
+
`Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
|
|
1238
|
+
`Termination stats: ${JSON.stringify({IN: this.terminationStats.incoming, OUT: this.terminationStats.outgoing})}`
|
|
666
1239
|
);
|
|
667
|
-
},
|
|
1240
|
+
}, this.settings.inactivityCheckInterval || 30000);
|
|
1241
|
+
|
|
1242
|
+
// Make sure the interval doesn't keep the process alive
|
|
1243
|
+
if (this.connectionLogger.unref) {
|
|
1244
|
+
this.connectionLogger.unref();
|
|
1245
|
+
}
|
|
668
1246
|
}
|
|
669
1247
|
|
|
1248
|
+
/**
|
|
1249
|
+
* Gracefully shut down the proxy
|
|
1250
|
+
*/
|
|
670
1251
|
public async stop() {
|
|
671
1252
|
console.log("PortProxy shutting down...");
|
|
672
1253
|
this.isShuttingDown = true;
|
|
@@ -675,7 +1256,16 @@ export class PortProxy {
|
|
|
675
1256
|
const closeServerPromises: Promise<void>[] = this.netServers.map(
|
|
676
1257
|
server =>
|
|
677
1258
|
new Promise<void>((resolve) => {
|
|
678
|
-
server.
|
|
1259
|
+
if (!server.listening) {
|
|
1260
|
+
resolve();
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
server.close((err) => {
|
|
1264
|
+
if (err) {
|
|
1265
|
+
console.log(`Error closing server: ${err.message}`);
|
|
1266
|
+
}
|
|
1267
|
+
resolve();
|
|
1268
|
+
});
|
|
679
1269
|
})
|
|
680
1270
|
);
|
|
681
1271
|
|
|
@@ -689,47 +1279,75 @@ export class PortProxy {
|
|
|
689
1279
|
await Promise.all(closeServerPromises);
|
|
690
1280
|
console.log("All servers closed. Cleaning up active connections...");
|
|
691
1281
|
|
|
692
|
-
//
|
|
1282
|
+
// Force destroy all active connections immediately
|
|
693
1283
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
694
1284
|
console.log(`Cleaning up ${connectionIds.length} active connections...`);
|
|
695
1285
|
|
|
1286
|
+
// First pass: End all connections gracefully
|
|
696
1287
|
for (const id of connectionIds) {
|
|
697
1288
|
const record = this.connectionRecords.get(id);
|
|
698
|
-
if (record
|
|
699
|
-
|
|
1289
|
+
if (record) {
|
|
1290
|
+
try {
|
|
1291
|
+
// Clear any timers
|
|
1292
|
+
if (record.cleanupTimer) {
|
|
1293
|
+
clearTimeout(record.cleanupTimer);
|
|
1294
|
+
record.cleanupTimer = undefined;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// End sockets gracefully
|
|
1298
|
+
if (record.incoming && !record.incoming.destroyed) {
|
|
1299
|
+
record.incoming.end();
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
1303
|
+
record.outgoing.end();
|
|
1304
|
+
}
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
console.log(`Error during graceful connection end for ${id}: ${err}`);
|
|
1307
|
+
}
|
|
700
1308
|
}
|
|
701
1309
|
}
|
|
702
1310
|
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
setTimeout(() => {
|
|
715
|
-
clearInterval(checkInterval);
|
|
716
|
-
if (this.connectionRecords.size > 0) {
|
|
717
|
-
console.log(`Forcing shutdown with ${this.connectionRecords.size} connections still active`);
|
|
718
|
-
|
|
719
|
-
// Force destroy any remaining connections
|
|
720
|
-
for (const record of this.connectionRecords.values()) {
|
|
1311
|
+
// Short delay to allow graceful ends to process
|
|
1312
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1313
|
+
|
|
1314
|
+
// Second pass: Force destroy everything
|
|
1315
|
+
for (const id of connectionIds) {
|
|
1316
|
+
const record = this.connectionRecords.get(id);
|
|
1317
|
+
if (record) {
|
|
1318
|
+
try {
|
|
1319
|
+
// Remove all listeners to prevent memory leaks
|
|
1320
|
+
if (record.incoming) {
|
|
1321
|
+
record.incoming.removeAllListeners();
|
|
721
1322
|
if (!record.incoming.destroyed) {
|
|
722
1323
|
record.incoming.destroy();
|
|
723
1324
|
}
|
|
724
|
-
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
if (record.outgoing) {
|
|
1328
|
+
record.outgoing.removeAllListeners();
|
|
1329
|
+
if (!record.outgoing.destroyed) {
|
|
725
1330
|
record.outgoing.destroy();
|
|
726
1331
|
}
|
|
727
1332
|
}
|
|
728
|
-
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
console.log(`Error during forced connection destruction for ${id}: ${err}`);
|
|
729
1335
|
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Clear all tracking maps
|
|
1340
|
+
this.connectionRecords.clear();
|
|
1341
|
+
this.domainTargetIndices.clear();
|
|
1342
|
+
this.connectionsByIP.clear();
|
|
1343
|
+
this.connectionRateByIP.clear();
|
|
1344
|
+
this.netServers = [];
|
|
1345
|
+
|
|
1346
|
+
// Reset termination stats
|
|
1347
|
+
this.terminationStats = {
|
|
1348
|
+
incoming: {},
|
|
1349
|
+
outgoing: {}
|
|
1350
|
+
};
|
|
733
1351
|
|
|
734
1352
|
console.log("PortProxy shutdown complete.");
|
|
735
1353
|
}
|