@push.rocks/smartproxy 3.24.0 → 3.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,17 +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
26
43
 
27
44
  // Socket optimization settings
28
- noDelay?: boolean; // Disable Nagle's algorithm (default: true)
29
- keepAlive?: boolean; // Enable TCP keepalive (default: true)
30
- keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
31
- maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
32
- initialDataTimeout?: number; // Timeout for initial data/SNI (ms)
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
33
88
  }
34
89
 
35
90
  /**
@@ -95,21 +150,6 @@ function extractSNI(buffer: Buffer): string | undefined {
95
150
  return undefined;
96
151
  }
97
152
 
98
- interface IConnectionRecord {
99
- id: string; // Unique connection identifier
100
- incoming: plugins.net.Socket;
101
- outgoing: plugins.net.Socket | null;
102
- incomingStartTime: number;
103
- outgoingStartTime?: number;
104
- outgoingClosedTime?: number;
105
- lockedDomain?: string; // Used to lock this connection to the initial SNI
106
- connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
107
- cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
108
- lastActivity: number; // Last activity timestamp for inactivity detection
109
- pendingData: Buffer[]; // Buffer to hold data during connection setup
110
- pendingDataSize: number; // Track total size of pending data
111
- }
112
-
113
153
  // Helper: Check if a port falls within any of the given port ranges
114
154
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
115
155
  return ranges.some(range => port >= range.from && port <= range.to);
@@ -145,6 +185,34 @@ const generateConnectionId = (): string => {
145
185
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
146
186
  };
147
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
+
148
216
  export class PortProxy {
149
217
  private netServers: plugins.net.Server[] = [];
150
218
  settings: IPortProxySettings;
@@ -155,6 +223,7 @@ export class PortProxy {
155
223
  // Map to track round robin indices for each domain config
156
224
  private domainTargetIndices: Map<IDomainConfig, number> = new Map();
157
225
 
226
+ // Enhanced stats tracking
158
227
  private terminationStats: {
159
228
  incoming: Record<string, number>;
160
229
  outgoing: Record<string, number>;
@@ -162,25 +231,218 @@ export class PortProxy {
162
231
  incoming: {},
163
232
  outgoing: {},
164
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();
165
238
 
166
239
  constructor(settingsArg: IPortProxySettings) {
240
+ // Set reasonable defaults for all settings
167
241
  this.settings = {
168
242
  ...settingsArg,
169
243
  targetIP: settingsArg.targetIP || 'localhost',
170
- maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
171
- 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
172
259
  noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
173
260
  keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
174
261
  keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 60000, // 1 minute
175
262
  maxPendingDataSize: settingsArg.maxPendingDataSize || 1024 * 1024, // 1MB
176
- initialDataTimeout: settingsArg.initialDataTimeout || 5000 // 5 seconds
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
177
273
  };
178
274
  }
179
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
+ */
180
330
  private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
181
331
  this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
182
332
  }
183
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
+
184
446
  /**
185
447
  * Cleans up a connection record.
186
448
  * Destroys both incoming and outgoing sockets, clears timers, and removes the record.
@@ -191,11 +453,20 @@ export class PortProxy {
191
453
  if (!record.connectionClosed) {
192
454
  record.connectionClosed = true;
193
455
 
456
+ // Track connection termination
457
+ this.removeConnectionByIP(record.remoteIP, record.id);
458
+
194
459
  if (record.cleanupTimer) {
195
460
  clearTimeout(record.cleanupTimer);
196
461
  record.cleanupTimer = undefined;
197
462
  }
198
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
+
199
470
  try {
200
471
  if (!record.incoming.destroyed) {
201
472
  // Try graceful shutdown first, then force destroy after a short timeout
@@ -206,7 +477,7 @@ export class PortProxy {
206
477
  record.incoming.destroy();
207
478
  }
208
479
  } catch (err) {
209
- console.log(`Error destroying incoming socket: ${err}`);
480
+ console.log(`[${record.id}] Error destroying incoming socket: ${err}`);
210
481
  }
211
482
  }, 1000);
212
483
 
@@ -216,13 +487,13 @@ export class PortProxy {
216
487
  }
217
488
  }
218
489
  } catch (err) {
219
- console.log(`Error closing incoming socket: ${err}`);
490
+ console.log(`[${record.id}] Error closing incoming socket: ${err}`);
220
491
  try {
221
492
  if (!record.incoming.destroyed) {
222
493
  record.incoming.destroy();
223
494
  }
224
495
  } catch (destroyErr) {
225
- console.log(`Error destroying incoming socket: ${destroyErr}`);
496
+ console.log(`[${record.id}] Error destroying incoming socket: ${destroyErr}`);
226
497
  }
227
498
  }
228
499
 
@@ -236,7 +507,7 @@ export class PortProxy {
236
507
  record.outgoing.destroy();
237
508
  }
238
509
  } catch (err) {
239
- console.log(`Error destroying outgoing socket: ${err}`);
510
+ console.log(`[${record.id}] Error destroying outgoing socket: ${err}`);
240
511
  }
241
512
  }, 1000);
242
513
 
@@ -246,13 +517,13 @@ export class PortProxy {
246
517
  }
247
518
  }
248
519
  } catch (err) {
249
- console.log(`Error closing outgoing socket: ${err}`);
520
+ console.log(`[${record.id}] Error closing outgoing socket: ${err}`);
250
521
  try {
251
522
  if (record.outgoing && !record.outgoing.destroyed) {
252
523
  record.outgoing.destroy();
253
524
  }
254
525
  } catch (destroyErr) {
255
- console.log(`Error destroying outgoing socket: ${destroyErr}`);
526
+ console.log(`[${record.id}] Error destroying outgoing socket: ${destroyErr}`);
256
527
  }
257
528
  }
258
529
 
@@ -263,15 +534,27 @@ export class PortProxy {
263
534
  // Remove the record from the tracking map
264
535
  this.connectionRecords.delete(record.id);
265
536
 
266
- const remoteIP = record.incoming.remoteAddress || 'unknown';
267
- 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
+ }
268
545
  }
269
546
  }
270
547
 
548
+ /**
549
+ * Update connection activity timestamp
550
+ */
271
551
  private updateActivity(record: IConnectionRecord): void {
272
552
  record.lastActivity = Date.now();
273
553
  }
274
554
 
555
+ /**
556
+ * Get target IP with round-robin support
557
+ */
275
558
  private getTargetIP(domainConfig: IDomainConfig): string {
276
559
  if (domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
277
560
  const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
@@ -282,12 +565,16 @@ export class PortProxy {
282
565
  return this.settings.targetIP!;
283
566
  }
284
567
 
568
+ /**
569
+ * Main method to start the proxy
570
+ */
285
571
  public async start() {
286
572
  // Don't start if already shutting down
287
573
  if (this.isShuttingDown) {
288
574
  console.log("Cannot start PortProxy while it's shutting down");
289
575
  return;
290
576
  }
577
+
291
578
  // Define a unified connection handler for all listening ports.
292
579
  const connectionHandler = (socket: plugins.net.Socket) => {
293
580
  if (this.isShuttingDown) {
@@ -297,12 +584,31 @@ export class PortProxy {
297
584
  }
298
585
 
299
586
  const remoteIP = socket.remoteAddress || '';
300
- 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
+ }
301
604
 
302
605
  // Apply socket optimizations
303
606
  socket.setNoDelay(this.settings.noDelay);
304
- socket.setKeepAlive(this.settings.keepAlive, this.settings.keepAliveInitialDelay);
607
+ if (this.settings.keepAlive) {
608
+ socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
609
+ }
305
610
 
611
+ // Create a unique connection ID and record
306
612
  const connectionId = generateConnectionId();
307
613
  const connectionRecord: IConnectionRecord = {
308
614
  id: connectionId,
@@ -311,12 +617,28 @@ export class PortProxy {
311
617
  incomingStartTime: Date.now(),
312
618
  lastActivity: Date.now(),
313
619
  connectionClosed: false,
314
- pendingData: [], // Initialize buffer for pending data
315
- pendingDataSize: 0 // Initialize buffer size counter
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
316
631
  };
632
+
633
+ // Track connection by IP
634
+ this.trackConnectionByIP(remoteIP, connectionId);
317
635
  this.connectionRecords.set(connectionId, connectionRecord);
318
636
 
319
- 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
+ }
320
642
 
321
643
  let initialDataReceived = false;
322
644
  let incomingTerminationReason: string | null = null;
@@ -327,15 +649,21 @@ export class PortProxy {
327
649
  this.cleanupConnection(connectionRecord);
328
650
  };
329
651
 
330
- // Define initiateCleanupOnce for compatibility with potential future improvements
652
+ // Define initiateCleanupOnce for compatibility
331
653
  const initiateCleanupOnce = (reason: string = 'normal') => {
332
- 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
+ }
333
661
  cleanupOnce();
334
662
  };
335
663
 
336
664
  // Helper to reject an incoming connection
337
665
  const rejectIncomingConnection = (reason: string, logMessage: string) => {
338
- console.log(logMessage);
666
+ console.log(`[${connectionId}] ${logMessage}`);
339
667
  socket.end();
340
668
  if (incomingTerminationReason === null) {
341
669
  incomingTerminationReason = reason;
@@ -349,7 +677,7 @@ export class PortProxy {
349
677
  if (this.settings.sniEnabled) {
350
678
  initialTimeout = setTimeout(() => {
351
679
  if (!initialDataReceived) {
352
- console.log(`Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`);
680
+ console.log(`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`);
353
681
  if (incomingTerminationReason === null) {
354
682
  incomingTerminationReason = 'initial_timeout';
355
683
  this.incrementTerminationStat('incoming', 'initial_timeout');
@@ -357,24 +685,75 @@ export class PortProxy {
357
685
  socket.end();
358
686
  cleanupOnce();
359
687
  }
360
- }, this.settings.initialDataTimeout || 5000);
688
+ }, this.settings.initialDataTimeout);
361
689
  } else {
362
690
  initialDataReceived = true;
363
691
  }
364
692
 
365
693
  socket.on('error', (err: Error) => {
366
- 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
+ }
367
737
  });
368
738
 
369
739
  const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
370
740
  const code = (err as any).code;
371
741
  let reason = 'error';
742
+
743
+ const now = Date.now();
744
+ const connectionDuration = now - connectionRecord.incomingStartTime;
745
+ const lastActivityAge = now - connectionRecord.lastActivity;
746
+
372
747
  if (code === 'ECONNRESET') {
373
748
  reason = 'econnreset';
374
- 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`);
375
753
  } else {
376
- 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`);
377
755
  }
756
+
378
757
  if (side === 'incoming' && incomingTerminationReason === null) {
379
758
  incomingTerminationReason = reason;
380
759
  this.incrementTerminationStat('incoming', reason);
@@ -382,11 +761,15 @@ export class PortProxy {
382
761
  outgoingTerminationReason = reason;
383
762
  this.incrementTerminationStat('outgoing', reason);
384
763
  }
764
+
385
765
  initiateCleanupOnce(reason);
386
766
  };
387
767
 
388
768
  const handleClose = (side: 'incoming' | 'outgoing') => () => {
389
- 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
+
390
773
  if (side === 'incoming' && incomingTerminationReason === null) {
391
774
  incomingTerminationReason = 'normal';
392
775
  this.incrementTerminationStat('incoming', 'normal');
@@ -396,6 +779,7 @@ export class PortProxy {
396
779
  // Record the time when outgoing socket closed.
397
780
  connectionRecord.outgoingClosedTime = Date.now();
398
781
  }
782
+
399
783
  initiateCleanupOnce('closed_' + side);
400
784
  };
401
785
 
@@ -413,6 +797,11 @@ export class PortProxy {
413
797
  initialTimeout = null;
414
798
  }
415
799
 
800
+ // Detect protocol if initial chunk is available
801
+ if (initialChunk && this.settings.enableProtocolDetection) {
802
+ this.detectProtocol(initialChunk, connectionRecord);
803
+ }
804
+
416
805
  // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
417
806
  const domainConfig = forcedDomain
418
807
  ? forcedDomain
@@ -455,11 +844,19 @@ export class PortProxy {
455
844
 
456
845
  // Temporary handler to collect data during connection setup
457
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
+
458
855
  // Check if adding this chunk would exceed the buffer limit
459
856
  const newSize = connectionRecord.pendingDataSize + chunk.length;
460
857
 
461
858
  if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
462
- console.log(`Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`);
859
+ console.log(`[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`);
463
860
  socket.end(); // Gracefully close the socket
464
861
  return initiateCleanupOnce('buffer_limit_exceeded');
465
862
  }
@@ -475,6 +872,7 @@ export class PortProxy {
475
872
 
476
873
  // Add initial chunk to pending data if present
477
874
  if (initialChunk) {
875
+ connectionRecord.bytesReceived += initialChunk.length;
478
876
  connectionRecord.pendingData.push(Buffer.from(initialChunk));
479
877
  connectionRecord.pendingDataSize = initialChunk.length;
480
878
  }
@@ -486,25 +884,27 @@ export class PortProxy {
486
884
 
487
885
  // Apply socket optimizations
488
886
  targetSocket.setNoDelay(this.settings.noDelay);
489
- targetSocket.setKeepAlive(this.settings.keepAlive, this.settings.keepAliveInitialDelay);
887
+ if (this.settings.keepAlive) {
888
+ targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
889
+ }
490
890
 
491
891
  // Setup specific error handler for connection phase
492
892
  targetSocket.once('error', (err) => {
493
893
  // This handler runs only once during the initial connection phase
494
894
  const code = (err as any).code;
495
- console.log(`Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`);
895
+ console.log(`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`);
496
896
 
497
897
  // Resume the incoming socket to prevent it from hanging
498
898
  socket.resume();
499
899
 
500
900
  if (code === 'ECONNREFUSED') {
501
- console.log(`Target ${targetHost}:${connectionOptions.port} refused connection`);
901
+ console.log(`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`);
502
902
  } else if (code === 'ETIMEDOUT') {
503
- console.log(`Connection to ${targetHost}:${connectionOptions.port} timed out`);
903
+ console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`);
504
904
  } else if (code === 'ECONNRESET') {
505
- console.log(`Connection to ${targetHost}:${connectionOptions.port} was reset`);
905
+ console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`);
506
906
  } else if (code === 'EHOSTUNREACH') {
507
- console.log(`Host ${targetHost} is unreachable`);
907
+ console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
508
908
  }
509
909
 
510
910
  // Clear any existing error handler after connection phase
@@ -528,15 +928,16 @@ export class PortProxy {
528
928
 
529
929
  // Handle timeouts
530
930
  socket.on('timeout', () => {
531
- 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)}`);
532
932
  if (incomingTerminationReason === null) {
533
933
  incomingTerminationReason = 'timeout';
534
934
  this.incrementTerminationStat('incoming', 'timeout');
535
935
  }
536
936
  initiateCleanupOnce('timeout_incoming');
537
937
  });
938
+
538
939
  targetSocket.on('timeout', () => {
539
- 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)}`);
540
941
  if (outgoingTerminationReason === null) {
541
942
  outgoingTerminationReason = 'timeout';
542
943
  this.incrementTerminationStat('outgoing', 'timeout');
@@ -544,9 +945,15 @@ export class PortProxy {
544
945
  initiateCleanupOnce('timeout_outgoing');
545
946
  });
546
947
 
547
- // Set appropriate timeouts
548
- socket.setTimeout(120000);
549
- 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
+ });
550
957
 
551
958
  // Wait for the outgoing connection to be ready before setting up piping
552
959
  targetSocket.once('connect', () => {
@@ -564,7 +971,7 @@ export class PortProxy {
564
971
  const combinedData = Buffer.concat(connectionRecord.pendingData);
565
972
  targetSocket.write(combinedData, (err) => {
566
973
  if (err) {
567
- console.log(`Error writing pending data to target: ${err.message}`);
974
+ console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
568
975
  return initiateCleanupOnce('write_error');
569
976
  }
570
977
 
@@ -573,10 +980,18 @@ export class PortProxy {
573
980
  targetSocket.pipe(socket);
574
981
  socket.resume(); // Resume the socket after piping is established
575
982
 
576
- console.log(
577
- `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
578
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
579
- );
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
+ }
580
995
  });
581
996
  } else {
582
997
  // No pending data, so just set up piping
@@ -584,27 +999,25 @@ export class PortProxy {
584
999
  targetSocket.pipe(socket);
585
1000
  socket.resume(); // Resume the socket after piping is established
586
1001
 
587
- console.log(
588
- `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
589
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
590
- );
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
+ }
591
1014
  }
592
1015
 
593
1016
  // Clear the buffer now that we've processed it
594
1017
  connectionRecord.pendingData = [];
595
1018
  connectionRecord.pendingDataSize = 0;
596
1019
 
597
- // Set up activity tracking
598
- socket.on('data', () => {
599
- connectionRecord.lastActivity = Date.now();
600
- });
601
-
602
- targetSocket.on('data', () => {
603
- connectionRecord.lastActivity = Date.now();
604
- });
605
-
606
- // Add the renegotiation listener (we don't need setImmediate here anymore
607
- // since we're already in the connect callback)
1020
+ // Add the renegotiation listener for SNI validation
608
1021
  if (serverName) {
609
1022
  socket.on('data', (renegChunk: Buffer) => {
610
1023
  if (renegChunk.length > 0 && renegChunk.readUInt8(0) === 22) {
@@ -612,26 +1025,30 @@ export class PortProxy {
612
1025
  // Try to extract SNI from potential renegotiation
613
1026
  const newSNI = extractSNI(renegChunk);
614
1027
  if (newSNI && newSNI !== connectionRecord.lockedDomain) {
615
- 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.`);
616
1029
  initiateCleanupOnce('sni_mismatch');
617
- } else if (newSNI) {
618
- 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.`);
619
1032
  }
620
1033
  } catch (err) {
621
- console.log(`Error processing potential renegotiation: ${err}. Allowing connection to continue.`);
1034
+ console.log(`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`);
622
1035
  }
623
1036
  }
624
1037
  });
625
1038
  }
626
- });
627
-
628
- // Initialize a cleanup timer for max connection lifetime
629
- 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);
630
1047
  connectionRecord.cleanupTimer = setTimeout(() => {
631
- console.log(`Connection from ${remoteIP} exceeded max lifetime (${this.settings.maxConnectionLifetime}ms), forcing cleanup.`);
632
- initiateCleanupOnce('max_lifetime');
633
- }, this.settings.maxConnectionLifetime);
634
- }
1048
+ console.log(`[${connectionId}] ${connectionRecord.protocolType} connection exceeded max lifetime (${plugins.prettyMs(protocolTimeout)}), forcing cleanup.`);
1049
+ initiateCleanupOnce(`${connectionRecord.protocolType}_max_lifetime`);
1050
+ }, protocolTimeout);
1051
+ });
635
1052
  };
636
1053
 
637
1054
  // --- PORT RANGE-BASED HANDLING ---
@@ -639,11 +1056,13 @@ export class PortProxy {
639
1056
  if (this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) {
640
1057
  if (this.settings.forwardAllGlobalRanges) {
641
1058
  if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
642
- 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.`);
643
1060
  socket.end();
644
1061
  return;
645
1062
  }
646
- 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
+ }
647
1066
  setupConnection('', undefined, {
648
1067
  domains: ['global'],
649
1068
  allowedIPs: this.settings.defaultAllowedIPs || [],
@@ -667,11 +1086,13 @@ export class PortProxy {
667
1086
  ...(this.settings.defaultBlockedIPs || [])
668
1087
  ];
669
1088
  if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
670
- 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}.`);
671
1090
  socket.end();
672
1091
  return;
673
1092
  }
674
- 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
+ }
675
1096
  setupConnection('', undefined, forcedDomain, localPort);
676
1097
  return;
677
1098
  }
@@ -693,7 +1114,10 @@ export class PortProxy {
693
1114
  const serverName = extractSNI(chunk) || '';
694
1115
  // Lock the connection to the negotiated SNI.
695
1116
  connectionRecord.lockedDomain = serverName;
696
- 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
+ }
697
1121
 
698
1122
  setupConnection(serverName, chunk);
699
1123
  });
@@ -735,15 +1159,19 @@ export class PortProxy {
735
1159
  this.netServers.push(server);
736
1160
  }
737
1161
 
738
- // 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
739
1163
  this.connectionLogger = setInterval(() => {
740
1164
  // Immediately return if shutting down
741
1165
  if (this.isShuttingDown) return;
742
- if (this.isShuttingDown) return;
743
1166
 
744
1167
  const now = Date.now();
745
1168
  let maxIncoming = 0;
746
1169
  let maxOutgoing = 0;
1170
+ let httpConnections = 0;
1171
+ let wsConnections = 0;
1172
+ let tlsConnections = 0;
1173
+ let unknownConnections = 0;
1174
+ let pooledConnections = 0;
747
1175
 
748
1176
  // Create a copy of the keys to avoid modification during iteration
749
1177
  const connectionIds = [...this.connectionRecords.keys()];
@@ -752,6 +1180,19 @@ export class PortProxy {
752
1180
  const record = this.connectionRecords.get(id);
753
1181
  if (!record) continue;
754
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
+
755
1196
  maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
756
1197
  if (record.outgoingStartTime) {
757
1198
  maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
@@ -762,30 +1203,51 @@ export class PortProxy {
762
1203
  !record.incoming.destroyed &&
763
1204
  !record.connectionClosed &&
764
1205
  (now - record.outgoingClosedTime > 30000)) {
765
- const remoteIP = record.incoming.remoteAddress || 'unknown';
766
- 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.`);
767
1208
  this.cleanupConnection(record, 'parity_check');
768
1209
  }
769
1210
 
770
- // Inactivity check
771
- const inactivityTime = now - record.lastActivity;
772
- if (inactivityTime > 180000 && // 3 minutes
773
- !record.connectionClosed) {
774
- const remoteIP = record.incoming.remoteAddress || 'unknown';
775
- console.log(`Inactivity check: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
776
- 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 = Math.floor(Math.random() * (1800000 - 1200000 + 1)) + 1200000; // random between 20 and 30 minutes
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
+ }
777
1230
  }
778
1231
  }
779
1232
 
1233
+ // Log detailed stats periodically
780
1234
  console.log(
781
- `(Interval Log) Active connections: ${this.connectionRecords.size}. ` +
782
- `Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. ` +
783
- `Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, ` +
784
- `(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})}`
785
1239
  );
786
- }, 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
+ }
787
1246
  }
788
1247
 
1248
+ /**
1249
+ * Gracefully shut down the proxy
1250
+ */
789
1251
  public async stop() {
790
1252
  console.log("PortProxy shutting down...");
791
1253
  this.isShuttingDown = true;
@@ -874,13 +1336,11 @@ export class PortProxy {
874
1336
  }
875
1337
  }
876
1338
 
877
- // Clear the connection records map
1339
+ // Clear all tracking maps
878
1340
  this.connectionRecords.clear();
879
-
880
- // Clear the domain target indices map to prevent memory leaks
881
1341
  this.domainTargetIndices.clear();
882
-
883
- // Clear any servers array
1342
+ this.connectionsByIP.clear();
1343
+ this.connectionRateByIP.clear();
884
1344
  this.netServers = [];
885
1345
 
886
1346
  // Reset termination stats