@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.
@@ -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
- maxConnectionLifetime?: number; // (ms) force cleanup of long-lived connections
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; // When true, forwards all connections on global port ranges to the global targetIP
25
- gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
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
- maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
163
- gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
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
- if (record && !record.incoming.destroyed) {
192
- record.incoming.destroy();
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
- if (!record.incoming.destroyed) {
199
- record.incoming.destroy();
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
- if (record && record.outgoing && !record.outgoing.destroyed) {
209
- record.outgoing.destroy();
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
- if (record.outgoing && !record.outgoing.destroyed) {
216
- record.outgoing.destroy();
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
- const remoteIP = record.incoming.remoteAddress || 'unknown';
224
- console.log(`Connection from ${remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`);
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: [] // Initialize buffer for pending data
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
- console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
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 with potential future improvements
652
+ // Define initiateCleanupOnce for compatibility
278
653
  const initiateCleanupOnce = (reason: string = 'normal') => {
279
- console.log(`Connection cleanup initiated for ${remoteIP} (${reason})`);
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
- }, 5000);
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
- console.log(`Connection closed on ${side} side from ${remoteIP}`);
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
- // Setup error handlers immediately
416
- socket.on('error', handleError('incoming'));
417
- targetSocket.on('error', handleError('outgoing'));
418
- socket.on('close', handleClose('incoming'));
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(120000);
441
- targetSocket.setTimeout(120000);
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
- console.log(
462
- `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
463
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
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
- console.log(
472
- `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
473
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
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
- // Set up activity tracking
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
- // Initialize a cleanup timer for max connection lifetime
512
- if (this.settings.maxConnectionLifetime) {
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(`Connection from ${remoteIP} exceeded max lifetime (${this.settings.maxConnectionLifetime}ms), forcing cleanup.`);
515
- initiateCleanupOnce('max_lifetime');
516
- }, this.settings.maxConnectionLifetime);
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
- console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
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
- console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
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
- console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
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 every 10 seconds.
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.incoming.remoteAddress || 'unknown';
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
- // Inactivity check
652
- const inactivityTime = now - record.lastActivity;
653
- if (inactivityTime > 180000 && // 3 minutes
654
- !record.connectionClosed) {
655
- const remoteIP = record.incoming.remoteAddress || 'unknown';
656
- console.log(`Inactivity check: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
657
- this.cleanupConnection(record, 'inactivity');
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
- `(Interval Log) Active connections: ${this.connectionRecords.size}. ` +
663
- `Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. ` +
664
- `Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, ` +
665
- `(outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`
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
- }, 10000);
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.close(() => resolve());
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
- // Clean up active connections
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 && !record.connectionClosed) {
699
- this.cleanupConnection(record, 'shutdown');
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
- // Wait for graceful shutdown or timeout
704
- const shutdownTimeout = this.settings.gracefulShutdownTimeout || 30000;
705
- await new Promise<void>((resolve) => {
706
- const checkInterval = setInterval(() => {
707
- if (this.connectionRecords.size === 0) {
708
- clearInterval(checkInterval);
709
- resolve(); // lets resolve here as early as we reach 0 remaining connections
710
- }
711
- }, 1000);
712
-
713
- // Force resolve after timeout
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
- if (record.outgoing && !record.outgoing.destroyed) {
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
- this.connectionRecords.clear();
1333
+ } catch (err) {
1334
+ console.log(`Error during forced connection destruction for ${id}: ${err}`);
729
1335
  }
730
- resolve();
731
- }, shutdownTimeout);
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
  }