@push.rocks/smartproxy 3.28.0 → 3.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,10 @@ import * as plugins from './plugins.js';
2
2
 
3
3
  /** Domain configuration with per-domain allowed port ranges */
4
4
  export interface IDomainConfig {
5
- domains: string[]; // Glob patterns for domain(s)
6
- allowedIPs: string[]; // Glob patterns for allowed IPs
7
- blockedIPs?: string[]; // Glob patterns for blocked IPs
8
- targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
5
+ domains: string[]; // Glob patterns for domain(s)
6
+ allowedIPs: string[]; // Glob patterns for allowed IPs
7
+ blockedIPs?: string[]; // Glob patterns for blocked IPs
8
+ targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
9
9
  portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
10
10
  // Allow domain-specific timeout override
11
11
  connectionTimeout?: number; // Connection timeout override (ms)
@@ -21,33 +21,33 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
21
21
  defaultAllowedIPs?: string[];
22
22
  defaultBlockedIPs?: string[];
23
23
  preserveSourceIP?: boolean;
24
-
24
+
25
25
  // Timeout settings
26
- initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
27
- socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
28
- inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
29
- maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
30
- inactivityTimeout?: number; // Inactivity timeout (ms), default: 3600000 (1h)
31
-
32
- gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
26
+ initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
27
+ socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
28
+ inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
29
+ maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 3600000 (1h)
30
+ inactivityTimeout?: number; // Inactivity timeout (ms), default: 3600000 (1h)
31
+
32
+ gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
33
33
  globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
34
- forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
35
-
34
+ forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
35
+
36
36
  // Socket optimization settings
37
- noDelay?: boolean; // Disable Nagle's algorithm (default: true)
38
- keepAlive?: boolean; // Enable TCP keepalive (default: true)
39
- keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
40
- maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
41
-
37
+ noDelay?: boolean; // Disable Nagle's algorithm (default: true)
38
+ keepAlive?: boolean; // Enable TCP keepalive (default: true)
39
+ keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
40
+ maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
41
+
42
42
  // Enhanced features
43
- disableInactivityCheck?: boolean; // Disable inactivity checking entirely
44
- enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
45
- enableDetailedLogging?: boolean; // Enable detailed connection logging
46
- enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
43
+ disableInactivityCheck?: boolean; // Disable inactivity checking entirely
44
+ enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
45
+ enableDetailedLogging?: boolean; // Enable detailed connection logging
46
+ enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
47
47
  enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
48
-
48
+
49
49
  // Rate limiting and security
50
- maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
50
+ maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
51
51
  connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
52
52
  }
53
53
 
@@ -55,28 +55,28 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
55
55
  * Enhanced connection record
56
56
  */
57
57
  interface IConnectionRecord {
58
- id: string; // Unique connection identifier
58
+ id: string; // Unique connection identifier
59
59
  incoming: plugins.net.Socket;
60
60
  outgoing: plugins.net.Socket | null;
61
61
  incomingStartTime: number;
62
62
  outgoingStartTime?: number;
63
63
  outgoingClosedTime?: number;
64
- lockedDomain?: string; // Used to lock this connection to the initial SNI
65
- connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
66
- cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
67
- lastActivity: number; // Last activity timestamp for inactivity detection
68
- pendingData: Buffer[]; // Buffer to hold data during connection setup
69
- pendingDataSize: number; // Track total size of pending data
70
-
64
+ lockedDomain?: string; // Used to lock this connection to the initial SNI
65
+ connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
66
+ cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
67
+ lastActivity: number; // Last activity timestamp for inactivity detection
68
+ pendingData: Buffer[]; // Buffer to hold data during connection setup
69
+ pendingDataSize: number; // Track total size of pending data
70
+
71
71
  // Enhanced tracking fields
72
- bytesReceived: number; // Total bytes received
73
- bytesSent: number; // Total bytes sent
74
- remoteIP: string; // Remote IP (cached for logging after socket close)
75
- localPort: number; // Local port (cached for logging)
76
- isTLS: boolean; // Whether this connection is a TLS connection
77
- tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
78
- hasReceivedInitialData: boolean; // Whether initial data has been received
79
- domainConfig?: IDomainConfig; // Associated domain config for this connection
72
+ bytesReceived: number; // Total bytes received
73
+ bytesSent: number; // Total bytes sent
74
+ remoteIP: string; // Remote IP (cached for logging after socket close)
75
+ localPort: number; // Local port (cached for logging)
76
+ isTLS: boolean; // Whether this connection is a TLS connection
77
+ tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
78
+ hasReceivedInitialData: boolean; // Whether initial data has been received
79
+ domainConfig?: IDomainConfig; // Associated domain config for this connection
80
80
  }
81
81
 
82
82
  /**
@@ -90,7 +90,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
90
90
  try {
91
91
  // Check if buffer is too small for TLS
92
92
  if (buffer.length < 5) {
93
- if (enableLogging) console.log("Buffer too small for TLS header");
93
+ if (enableLogging) console.log('Buffer too small for TLS header');
94
94
  return undefined;
95
95
  }
96
96
 
@@ -105,11 +105,14 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
105
105
  const majorVersion = buffer.readUInt8(1);
106
106
  const minorVersion = buffer.readUInt8(2);
107
107
  if (enableLogging) console.log(`TLS Version: ${majorVersion}.${minorVersion}`);
108
-
108
+
109
109
  // Check record length
110
110
  const recordLength = buffer.readUInt16BE(3);
111
111
  if (buffer.length < 5 + recordLength) {
112
- if (enableLogging) console.log(`Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`);
112
+ if (enableLogging)
113
+ console.log(
114
+ `Buffer too small for TLS record. Expected: ${5 + recordLength}, Got: ${buffer.length}`
115
+ );
113
116
  return undefined;
114
117
  }
115
118
 
@@ -121,12 +124,12 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
121
124
  }
122
125
 
123
126
  offset += 4; // Skip handshake header (type + length)
124
-
127
+
125
128
  // Client version
126
129
  const clientMajorVersion = buffer.readUInt8(offset);
127
130
  const clientMinorVersion = buffer.readUInt8(offset + 1);
128
131
  if (enableLogging) console.log(`Client Version: ${clientMajorVersion}.${clientMinorVersion}`);
129
-
132
+
130
133
  offset += 2 + 32; // Skip client version and random
131
134
 
132
135
  // Session ID
@@ -136,7 +139,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
136
139
 
137
140
  // Cipher suites
138
141
  if (offset + 2 > buffer.length) {
139
- if (enableLogging) console.log("Buffer too small for cipher suites length");
142
+ if (enableLogging) console.log('Buffer too small for cipher suites length');
140
143
  return undefined;
141
144
  }
142
145
  const cipherSuitesLength = buffer.readUInt16BE(offset);
@@ -145,7 +148,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
145
148
 
146
149
  // Compression methods
147
150
  if (offset + 1 > buffer.length) {
148
- if (enableLogging) console.log("Buffer too small for compression methods length");
151
+ if (enableLogging) console.log('Buffer too small for compression methods length');
149
152
  return undefined;
150
153
  }
151
154
  const compressionMethodsLength = buffer.readUInt8(offset);
@@ -154,7 +157,7 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
154
157
 
155
158
  // Extensions
156
159
  if (offset + 2 > buffer.length) {
157
- if (enableLogging) console.log("Buffer too small for extensions length");
160
+ if (enableLogging) console.log('Buffer too small for extensions length');
158
161
  return undefined;
159
162
  }
160
163
  const extensionsLength = buffer.readUInt16BE(offset);
@@ -163,7 +166,10 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
163
166
  const extensionsEnd = offset + extensionsLength;
164
167
 
165
168
  if (extensionsEnd > buffer.length) {
166
- if (enableLogging) console.log(`Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`);
169
+ if (enableLogging)
170
+ console.log(
171
+ `Buffer too small for extensions. Expected end: ${extensionsEnd}, Buffer length: ${buffer.length}`
172
+ );
167
173
  return undefined;
168
174
  }
169
175
 
@@ -171,45 +177,56 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
171
177
  while (offset + 4 <= extensionsEnd) {
172
178
  const extensionType = buffer.readUInt16BE(offset);
173
179
  const extensionLength = buffer.readUInt16BE(offset + 2);
174
-
175
- if (enableLogging) console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
176
-
180
+
181
+ if (enableLogging)
182
+ console.log(`Extension Type: 0x${extensionType.toString(16)}, Length: ${extensionLength}`);
183
+
177
184
  offset += 4;
178
-
179
- if (extensionType === 0x0000) { // SNI extension
185
+
186
+ if (extensionType === 0x0000) {
187
+ // SNI extension
180
188
  if (offset + 2 > buffer.length) {
181
- if (enableLogging) console.log("Buffer too small for SNI list length");
189
+ if (enableLogging) console.log('Buffer too small for SNI list length');
182
190
  return undefined;
183
191
  }
184
-
192
+
185
193
  const sniListLength = buffer.readUInt16BE(offset);
186
194
  if (enableLogging) console.log(`SNI List Length: ${sniListLength}`);
187
195
  offset += 2;
188
196
  const sniListEnd = offset + sniListLength;
189
-
197
+
190
198
  if (sniListEnd > buffer.length) {
191
- if (enableLogging) console.log(`Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`);
199
+ if (enableLogging)
200
+ console.log(
201
+ `Buffer too small for SNI list. Expected end: ${sniListEnd}, Buffer length: ${buffer.length}`
202
+ );
192
203
  return undefined;
193
204
  }
194
-
205
+
195
206
  while (offset + 3 < sniListEnd) {
196
207
  const nameType = buffer.readUInt8(offset++);
197
208
  const nameLen = buffer.readUInt16BE(offset);
198
209
  offset += 2;
199
-
210
+
200
211
  if (enableLogging) console.log(`Name Type: ${nameType}, Name Length: ${nameLen}`);
201
-
202
- if (nameType === 0) { // host_name
212
+
213
+ if (nameType === 0) {
214
+ // host_name
203
215
  if (offset + nameLen > buffer.length) {
204
- if (enableLogging) console.log(`Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${buffer.length}`);
216
+ if (enableLogging)
217
+ console.log(
218
+ `Buffer too small for hostname. Expected: ${offset + nameLen}, Got: ${
219
+ buffer.length
220
+ }`
221
+ );
205
222
  return undefined;
206
223
  }
207
-
224
+
208
225
  const serverName = buffer.toString('utf8', offset, offset + nameLen);
209
226
  if (enableLogging) console.log(`Extracted SNI: ${serverName}`);
210
227
  return serverName;
211
228
  }
212
-
229
+
213
230
  offset += nameLen;
214
231
  }
215
232
  break;
@@ -217,8 +234,8 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
217
234
  offset += extensionLength;
218
235
  }
219
236
  }
220
-
221
- if (enableLogging) console.log("No SNI extension found");
237
+
238
+ if (enableLogging) console.log('No SNI extension found');
222
239
  return undefined;
223
240
  } catch (err) {
224
241
  console.log(`Error extracting SNI: ${err}`);
@@ -228,13 +245,13 @@ function extractSNI(buffer: Buffer, enableLogging: boolean = false): string | un
228
245
 
229
246
  // Helper: Check if a port falls within any of the given port ranges
230
247
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
231
- return ranges.some(range => port >= range.from && port <= range.to);
248
+ return ranges.some((range) => port >= range.from && port <= range.to);
232
249
  };
233
250
 
234
251
  // Helper: Check if a given IP matches any of the glob patterns
235
252
  const isAllowed = (ip: string, patterns: string[]): boolean => {
236
253
  if (!ip || !patterns || patterns.length === 0) return false;
237
-
254
+
238
255
  const normalizeIP = (ip: string): string[] => {
239
256
  if (!ip) return [];
240
257
  if (ip.startsWith('::ffff:')) {
@@ -246,13 +263,13 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
246
263
  }
247
264
  return [ip];
248
265
  };
249
-
266
+
250
267
  const normalizedIPVariants = normalizeIP(ip);
251
268
  if (normalizedIPVariants.length === 0) return false;
252
-
269
+
253
270
  const expandedPatterns = patterns.flatMap(normalizeIP);
254
- return normalizedIPVariants.some(ipVariant =>
255
- expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
271
+ return normalizedIPVariants.some((ipVariant) =>
272
+ expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
256
273
  );
257
274
  };
258
275
 
@@ -297,7 +314,7 @@ export class PortProxy {
297
314
  incoming: {},
298
315
  outgoing: {},
299
316
  };
300
-
317
+
301
318
  // Connection tracking by IP for rate limiting
302
319
  private connectionsByIP: Map<string, Set<string>> = new Map();
303
320
  private connectionRateByIP: Map<string, number[]> = new Map();
@@ -307,29 +324,29 @@ export class PortProxy {
307
324
  this.settings = {
308
325
  ...settingsArg,
309
326
  targetIP: settingsArg.targetIP || 'localhost',
310
-
311
- // Timeout settings with our enhanced defaults
312
- initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial data
313
- socketTimeout: settingsArg.socketTimeout || 3600000, // 1 hour socket timeout
327
+
328
+ // Timeout settings with safe limits for Node.js
329
+ initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake
330
+ socketTimeout: settingsArg.socketTimeout || 2147483647, // Maximum safe value (~24.8 days)
314
331
  inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
315
- maxConnectionLifetime: settingsArg.maxConnectionLifetime || 3600000, // 1 hour default lifetime
316
- inactivityTimeout: settingsArg.inactivityTimeout || 3600000, // 1 hour inactivity timeout
317
-
332
+ maxConnectionLifetime: settingsArg.maxConnectionLifetime || 2147483647, // Maximum safe value (~24.8 days)
333
+ inactivityTimeout: settingsArg.inactivityTimeout || 14400000, // 4 hours inactivity timeout
334
+
318
335
  gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
319
-
336
+
320
337
  // Socket optimization settings
321
338
  noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
322
339
  keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
323
340
  keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 30000, // 30 seconds
324
341
  maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB to handle large TLS handshakes
325
-
342
+
326
343
  // Feature flags
327
344
  disableInactivityCheck: settingsArg.disableInactivityCheck || false,
328
345
  enableKeepAliveProbes: settingsArg.enableKeepAliveProbes || false,
329
346
  enableDetailedLogging: settingsArg.enableDetailedLogging || false,
330
347
  enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
331
348
  enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || true,
332
-
349
+
333
350
  // Rate limiting defaults
334
351
  maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, // 100 connections per IP
335
352
  connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, // 300 per minute
@@ -349,17 +366,17 @@ export class PortProxy {
349
366
  private checkConnectionRate(ip: string): boolean {
350
367
  const now = Date.now();
351
368
  const minute = 60 * 1000;
352
-
369
+
353
370
  if (!this.connectionRateByIP.has(ip)) {
354
371
  this.connectionRateByIP.set(ip, [now]);
355
372
  return true;
356
373
  }
357
-
374
+
358
375
  // Get timestamps and filter out entries older than 1 minute
359
- const timestamps = this.connectionRateByIP.get(ip)!.filter(time => now - time < minute);
376
+ const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
360
377
  timestamps.push(now);
361
378
  this.connectionRateByIP.set(ip, timestamps);
362
-
379
+
363
380
  // Check if rate exceeds limit
364
381
  return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
365
382
  }
@@ -402,14 +419,14 @@ export class PortProxy {
402
419
  if (record.domainConfig?.connectionTimeout) {
403
420
  return record.domainConfig.connectionTimeout;
404
421
  }
405
-
422
+
406
423
  // Use default timeout, potentially randomized
407
424
  const baseTimeout = this.settings.maxConnectionLifetime!;
408
-
425
+
409
426
  if (this.settings.enableRandomizedTimeouts) {
410
427
  return randomizeTimeout(baseTimeout);
411
428
  }
412
-
429
+
413
430
  return baseTimeout;
414
431
  }
415
432
 
@@ -422,20 +439,20 @@ export class PortProxy {
422
439
  private cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
423
440
  if (!record.connectionClosed) {
424
441
  record.connectionClosed = true;
425
-
442
+
426
443
  // Track connection termination
427
444
  this.removeConnectionByIP(record.remoteIP, record.id);
428
-
445
+
429
446
  if (record.cleanupTimer) {
430
447
  clearTimeout(record.cleanupTimer);
431
448
  record.cleanupTimer = undefined;
432
449
  }
433
-
450
+
434
451
  // Detailed logging data
435
452
  const duration = Date.now() - record.incomingStartTime;
436
453
  const bytesReceived = record.bytesReceived;
437
454
  const bytesSent = record.bytesSent;
438
-
455
+
439
456
  try {
440
457
  if (!record.incoming.destroyed) {
441
458
  // Try graceful shutdown first, then force destroy after a short timeout
@@ -449,7 +466,7 @@ export class PortProxy {
449
466
  console.log(`[${record.id}] Error destroying incoming socket: ${err}`);
450
467
  }
451
468
  }, 1000);
452
-
469
+
453
470
  // Ensure the timeout doesn't block Node from exiting
454
471
  if (incomingTimeout.unref) {
455
472
  incomingTimeout.unref();
@@ -465,7 +482,7 @@ export class PortProxy {
465
482
  console.log(`[${record.id}] Error destroying incoming socket: ${destroyErr}`);
466
483
  }
467
484
  }
468
-
485
+
469
486
  try {
470
487
  if (record.outgoing && !record.outgoing.destroyed) {
471
488
  // Try graceful shutdown first, then force destroy after a short timeout
@@ -479,7 +496,7 @@ export class PortProxy {
479
496
  console.log(`[${record.id}] Error destroying outgoing socket: ${err}`);
480
497
  }
481
498
  }, 1000);
482
-
499
+
483
500
  // Ensure the timeout doesn't block Node from exiting
484
501
  if (outgoingTimeout.unref) {
485
502
  outgoingTimeout.unref();
@@ -495,21 +512,27 @@ export class PortProxy {
495
512
  console.log(`[${record.id}] Error destroying outgoing socket: ${destroyErr}`);
496
513
  }
497
514
  }
498
-
515
+
499
516
  // Clear pendingData to avoid memory leaks
500
517
  record.pendingData = [];
501
518
  record.pendingDataSize = 0;
502
-
519
+
503
520
  // Remove the record from the tracking map
504
521
  this.connectionRecords.delete(record.id);
505
-
522
+
506
523
  // Log connection details
507
524
  if (this.settings.enableDetailedLogging) {
508
- console.log(`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
509
- ` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
510
- `TLS: ${record.isTLS ? 'Yes' : 'No'}`);
525
+ console.log(
526
+ `[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
527
+ ` Duration: ${plugins.prettyMs(
528
+ duration
529
+ )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
530
+ `TLS: ${record.isTLS ? 'Yes' : 'No'}`
531
+ );
511
532
  } else {
512
- console.log(`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`);
533
+ console.log(
534
+ `[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`
535
+ );
513
536
  }
514
537
  }
515
538
  }
@@ -543,7 +566,7 @@ export class PortProxy {
543
566
  console.log("Cannot start PortProxy while it's shutting down");
544
567
  return;
545
568
  }
546
-
569
+
547
570
  // Define a unified connection handler for all listening ports.
548
571
  const connectionHandler = (socket: plugins.net.Socket) => {
549
572
  if (this.isShuttingDown) {
@@ -554,29 +577,35 @@ export class PortProxy {
554
577
 
555
578
  const remoteIP = socket.remoteAddress || '';
556
579
  const localPort = socket.localPort || 0; // The port on which this connection was accepted.
557
-
580
+
558
581
  // Check rate limits
559
- if (this.settings.maxConnectionsPerIP &&
560
- this.getConnectionCountByIP(remoteIP) >= this.settings.maxConnectionsPerIP) {
561
- console.log(`Connection rejected from ${remoteIP}: Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`);
582
+ if (
583
+ this.settings.maxConnectionsPerIP &&
584
+ this.getConnectionCountByIP(remoteIP) >= this.settings.maxConnectionsPerIP
585
+ ) {
586
+ console.log(
587
+ `Connection rejected from ${remoteIP}: Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`
588
+ );
562
589
  socket.end();
563
590
  socket.destroy();
564
591
  return;
565
592
  }
566
-
593
+
567
594
  if (this.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(remoteIP)) {
568
- console.log(`Connection rejected from ${remoteIP}: Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`);
595
+ console.log(
596
+ `Connection rejected from ${remoteIP}: Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`
597
+ );
569
598
  socket.end();
570
599
  socket.destroy();
571
600
  return;
572
601
  }
573
-
602
+
574
603
  // Apply socket optimizations
575
604
  socket.setNoDelay(this.settings.noDelay);
576
605
  if (this.settings.keepAlive) {
577
606
  socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
578
607
  }
579
-
608
+
580
609
  // Apply enhanced TCP options if available
581
610
  if (this.settings.enableKeepAliveProbes) {
582
611
  try {
@@ -591,7 +620,7 @@ export class PortProxy {
591
620
  // Ignore errors - these are optional enhancements
592
621
  }
593
622
  }
594
-
623
+
595
624
  // Create a unique connection ID and record
596
625
  const connectionId = generateConnectionId();
597
626
  const connectionRecord: IConnectionRecord = {
@@ -603,7 +632,7 @@ export class PortProxy {
603
632
  connectionClosed: false,
604
633
  pendingData: [],
605
634
  pendingDataSize: 0,
606
-
635
+
607
636
  // Initialize enhanced tracking fields
608
637
  bytesReceived: 0,
609
638
  bytesSent: 0,
@@ -611,17 +640,21 @@ export class PortProxy {
611
640
  localPort: localPort,
612
641
  isTLS: false,
613
642
  tlsHandshakeComplete: false,
614
- hasReceivedInitialData: false
643
+ hasReceivedInitialData: false,
615
644
  };
616
-
645
+
617
646
  // Track connection by IP
618
647
  this.trackConnectionByIP(remoteIP, connectionId);
619
648
  this.connectionRecords.set(connectionId, connectionRecord);
620
-
649
+
621
650
  if (this.settings.enableDetailedLogging) {
622
- console.log(`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
651
+ console.log(
652
+ `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`
653
+ );
623
654
  } else {
624
- console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
655
+ console.log(
656
+ `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`
657
+ );
625
658
  }
626
659
 
627
660
  let initialDataReceived = false;
@@ -632,7 +665,7 @@ export class PortProxy {
632
665
  const cleanupOnce = () => {
633
666
  this.cleanupConnection(connectionRecord);
634
667
  };
635
-
668
+
636
669
  // Define initiateCleanupOnce for compatibility
637
670
  const initiateCleanupOnce = (reason: string = 'normal') => {
638
671
  if (this.settings.enableDetailedLogging) {
@@ -661,7 +694,9 @@ export class PortProxy {
661
694
  if (this.settings.sniEnabled) {
662
695
  initialTimeout = setTimeout(() => {
663
696
  if (!initialDataReceived) {
664
- console.log(`[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`);
697
+ console.log(
698
+ `[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
699
+ );
665
700
  if (incomingTerminationReason === null) {
666
701
  incomingTerminationReason = 'initial_timeout';
667
702
  this.incrementTerminationStat('incoming', 'initial_timeout');
@@ -670,7 +705,7 @@ export class PortProxy {
670
705
  cleanupOnce();
671
706
  }
672
707
  }, this.settings.initialDataTimeout!);
673
-
708
+
674
709
  // Make sure timeout doesn't keep the process alive
675
710
  if (initialTimeout.unref) {
676
711
  initialTimeout.unref();
@@ -688,13 +723,15 @@ export class PortProxy {
688
723
  socket.on('data', (chunk: Buffer) => {
689
724
  connectionRecord.bytesReceived += chunk.length;
690
725
  this.updateActivity(connectionRecord);
691
-
726
+
692
727
  // Check for TLS handshake if this is the first chunk
693
728
  if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
694
729
  connectionRecord.isTLS = true;
695
-
730
+
696
731
  if (this.settings.enableTlsDebugLogging) {
697
- console.log(`[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`);
732
+ console.log(
733
+ `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
734
+ );
698
735
  // Try to extract SNI and log detailed debug info
699
736
  extractSNI(chunk, true);
700
737
  }
@@ -704,21 +741,39 @@ export class PortProxy {
704
741
  const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
705
742
  const code = (err as any).code;
706
743
  let reason = 'error';
707
-
744
+
708
745
  const now = Date.now();
709
746
  const connectionDuration = now - connectionRecord.incomingStartTime;
710
747
  const lastActivityAge = now - connectionRecord.lastActivity;
711
-
748
+
712
749
  if (code === 'ECONNRESET') {
713
750
  reason = 'econnreset';
714
- console.log(`[${connectionId}] ECONNRESET on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`);
751
+ console.log(
752
+ `[${connectionId}] ECONNRESET on ${side} side from ${remoteIP}: ${
753
+ err.message
754
+ }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
755
+ lastActivityAge
756
+ )} ago`
757
+ );
715
758
  } else if (code === 'ETIMEDOUT') {
716
759
  reason = 'etimedout';
717
- console.log(`[${connectionId}] ETIMEDOUT on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`);
760
+ console.log(
761
+ `[${connectionId}] ETIMEDOUT on ${side} side from ${remoteIP}: ${
762
+ err.message
763
+ }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
764
+ lastActivityAge
765
+ )} ago`
766
+ );
718
767
  } else {
719
- console.log(`[${connectionId}] Error on ${side} side from ${remoteIP}: ${err.message}. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`);
768
+ console.log(
769
+ `[${connectionId}] Error on ${side} side from ${remoteIP}: ${
770
+ err.message
771
+ }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
772
+ lastActivityAge
773
+ )} ago`
774
+ );
720
775
  }
721
-
776
+
722
777
  if (side === 'incoming' && incomingTerminationReason === null) {
723
778
  incomingTerminationReason = reason;
724
779
  this.incrementTerminationStat('incoming', reason);
@@ -726,7 +781,7 @@ export class PortProxy {
726
781
  outgoingTerminationReason = reason;
727
782
  this.incrementTerminationStat('outgoing', reason);
728
783
  }
729
-
784
+
730
785
  initiateCleanupOnce(reason);
731
786
  };
732
787
 
@@ -734,7 +789,7 @@ export class PortProxy {
734
789
  if (this.settings.enableDetailedLogging) {
735
790
  console.log(`[${connectionId}] Connection closed on ${side} side from ${remoteIP}`);
736
791
  }
737
-
792
+
738
793
  if (side === 'incoming' && incomingTerminationReason === null) {
739
794
  incomingTerminationReason = 'normal';
740
795
  this.incrementTerminationStat('incoming', 'normal');
@@ -744,7 +799,7 @@ export class PortProxy {
744
799
  // Record the time when outgoing socket closed.
745
800
  connectionRecord.outgoingClosedTime = Date.now();
746
801
  }
747
-
802
+
748
803
  initiateCleanupOnce('closed_' + side);
749
804
  };
750
805
 
@@ -755,54 +810,80 @@ export class PortProxy {
755
810
  * @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
756
811
  * @param overridePort - If provided, use this port for the outgoing connection.
757
812
  */
758
- const setupConnection = (serverName: string, initialChunk?: Buffer, forcedDomain?: IDomainConfig, overridePort?: number) => {
813
+ const setupConnection = (
814
+ serverName: string,
815
+ initialChunk?: Buffer,
816
+ forcedDomain?: IDomainConfig,
817
+ overridePort?: number
818
+ ) => {
759
819
  // Clear the initial timeout since we've received data
760
820
  if (initialTimeout) {
761
821
  clearTimeout(initialTimeout);
762
822
  initialTimeout = null;
763
823
  }
764
-
824
+
765
825
  // Mark that we've received initial data
766
826
  initialDataReceived = true;
767
827
  connectionRecord.hasReceivedInitialData = true;
768
-
828
+
769
829
  // Check if this looks like a TLS handshake
770
830
  if (initialChunk && isTlsHandshake(initialChunk)) {
771
831
  connectionRecord.isTLS = true;
772
-
832
+
773
833
  if (this.settings.enableTlsDebugLogging) {
774
- console.log(`[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`);
834
+ console.log(
835
+ `[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
836
+ );
775
837
  }
776
838
  }
777
-
839
+
778
840
  // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
779
841
  const domainConfig = forcedDomain
780
842
  ? forcedDomain
781
- : (serverName ? this.settings.domainConfigs.find(config =>
782
- config.domains.some(d => plugins.minimatch(serverName, d))
783
- ) : undefined);
843
+ : serverName
844
+ ? this.settings.domainConfigs.find((config) =>
845
+ config.domains.some((d) => plugins.minimatch(serverName, d))
846
+ )
847
+ : undefined;
784
848
 
785
849
  // Save domain config in connection record
786
850
  connectionRecord.domainConfig = domainConfig;
787
-
851
+
788
852
  // IP validation is skipped if allowedIPs is empty
789
853
  if (domainConfig) {
790
854
  const effectiveAllowedIPs: string[] = [
791
855
  ...domainConfig.allowedIPs,
792
- ...(this.settings.defaultAllowedIPs || [])
856
+ ...(this.settings.defaultAllowedIPs || []),
793
857
  ];
794
858
  const effectiveBlockedIPs: string[] = [
795
859
  ...(domainConfig.blockedIPs || []),
796
- ...(this.settings.defaultBlockedIPs || [])
860
+ ...(this.settings.defaultBlockedIPs || []),
797
861
  ];
798
-
862
+
799
863
  // Skip IP validation if allowedIPs is empty
800
- if (domainConfig.allowedIPs.length > 0 && !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
801
- return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
864
+ if (
865
+ domainConfig.allowedIPs.length > 0 &&
866
+ !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)
867
+ ) {
868
+ return rejectIncomingConnection(
869
+ 'rejected',
870
+ `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(
871
+ ', '
872
+ )}`
873
+ );
802
874
  }
803
875
  } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
804
- if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
805
- return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
876
+ if (
877
+ !isGlobIPAllowed(
878
+ remoteIP,
879
+ this.settings.defaultAllowedIPs,
880
+ this.settings.defaultBlockedIPs || []
881
+ )
882
+ ) {
883
+ return rejectIncomingConnection(
884
+ 'rejected',
885
+ `Connection rejected: IP ${remoteIP} not allowed by default allowed list`
886
+ );
806
887
  }
807
888
  }
808
889
 
@@ -822,25 +903,29 @@ export class PortProxy {
822
903
  const tempDataHandler = (chunk: Buffer) => {
823
904
  // Track bytes received
824
905
  connectionRecord.bytesReceived += chunk.length;
825
-
906
+
826
907
  // Check for TLS handshake
827
908
  if (!connectionRecord.isTLS && isTlsHandshake(chunk)) {
828
909
  connectionRecord.isTLS = true;
829
-
910
+
830
911
  if (this.settings.enableTlsDebugLogging) {
831
- console.log(`[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`);
912
+ console.log(
913
+ `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
914
+ );
832
915
  }
833
916
  }
834
-
917
+
835
918
  // Check if adding this chunk would exceed the buffer limit
836
919
  const newSize = connectionRecord.pendingDataSize + chunk.length;
837
-
920
+
838
921
  if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
839
- console.log(`[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`);
922
+ console.log(
923
+ `[${connectionId}] Buffer limit exceeded for connection from ${remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
924
+ );
840
925
  socket.end(); // Gracefully close the socket
841
926
  return initiateCleanupOnce('buffer_limit_exceeded');
842
927
  }
843
-
928
+
844
929
  // Buffer the chunk and update the size counter
845
930
  connectionRecord.pendingData.push(Buffer.from(chunk));
846
931
  connectionRecord.pendingDataSize = newSize;
@@ -861,13 +946,13 @@ export class PortProxy {
861
946
  const targetSocket = plugins.net.connect(connectionOptions);
862
947
  connectionRecord.outgoing = targetSocket;
863
948
  connectionRecord.outgoingStartTime = Date.now();
864
-
949
+
865
950
  // Apply socket optimizations
866
951
  targetSocket.setNoDelay(this.settings.noDelay);
867
952
  if (this.settings.keepAlive) {
868
953
  targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
869
954
  }
870
-
955
+
871
956
  // Apply enhanced TCP options if available
872
957
  if (this.settings.enableKeepAliveProbes) {
873
958
  try {
@@ -881,57 +966,73 @@ export class PortProxy {
881
966
  // Ignore errors - these are optional enhancements
882
967
  }
883
968
  }
884
-
969
+
885
970
  // Setup specific error handler for connection phase
886
971
  targetSocket.once('error', (err) => {
887
972
  // This handler runs only once during the initial connection phase
888
973
  const code = (err as any).code;
889
- console.log(`[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`);
890
-
974
+ console.log(
975
+ `[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`
976
+ );
977
+
891
978
  // Resume the incoming socket to prevent it from hanging
892
979
  socket.resume();
893
-
980
+
894
981
  if (code === 'ECONNREFUSED') {
895
- console.log(`[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`);
982
+ console.log(
983
+ `[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
984
+ );
896
985
  } else if (code === 'ETIMEDOUT') {
897
- console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`);
986
+ console.log(
987
+ `[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`
988
+ );
898
989
  } else if (code === 'ECONNRESET') {
899
- console.log(`[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`);
990
+ console.log(
991
+ `[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`
992
+ );
900
993
  } else if (code === 'EHOSTUNREACH') {
901
994
  console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
902
995
  }
903
-
996
+
904
997
  // Clear any existing error handler after connection phase
905
998
  targetSocket.removeAllListeners('error');
906
-
999
+
907
1000
  // Re-add the normal error handler for established connections
908
1001
  targetSocket.on('error', handleError('outgoing'));
909
-
1002
+
910
1003
  if (outgoingTerminationReason === null) {
911
1004
  outgoingTerminationReason = 'connection_failed';
912
1005
  this.incrementTerminationStat('outgoing', 'connection_failed');
913
1006
  }
914
-
1007
+
915
1008
  // Clean up the connection
916
1009
  initiateCleanupOnce(`connection_failed_${code}`);
917
1010
  });
918
-
1011
+
919
1012
  // Setup close handler
920
1013
  targetSocket.on('close', handleClose('outgoing'));
921
1014
  socket.on('close', handleClose('incoming'));
922
-
1015
+
923
1016
  // Handle timeouts
924
1017
  socket.on('timeout', () => {
925
- console.log(`[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
1018
+ console.log(
1019
+ `[${connectionId}] Timeout on incoming side from ${remoteIP} after ${plugins.prettyMs(
1020
+ this.settings.socketTimeout || 3600000
1021
+ )}`
1022
+ );
926
1023
  if (incomingTerminationReason === null) {
927
1024
  incomingTerminationReason = 'timeout';
928
1025
  this.incrementTerminationStat('incoming', 'timeout');
929
1026
  }
930
1027
  initiateCleanupOnce('timeout_incoming');
931
1028
  });
932
-
1029
+
933
1030
  targetSocket.on('timeout', () => {
934
- console.log(`[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`);
1031
+ console.log(
1032
+ `[${connectionId}] Timeout on outgoing side from ${remoteIP} after ${plugins.prettyMs(
1033
+ this.settings.socketTimeout || 3600000
1034
+ )}`
1035
+ );
935
1036
  if (outgoingTerminationReason === null) {
936
1037
  outgoingTerminationReason = 'timeout';
937
1038
  this.incrementTerminationStat('outgoing', 'timeout');
@@ -942,48 +1043,62 @@ export class PortProxy {
942
1043
  // Set appropriate timeouts using the configured value
943
1044
  socket.setTimeout(this.settings.socketTimeout || 3600000);
944
1045
  targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
945
-
1046
+
946
1047
  // Track outgoing data for bytes counting
947
1048
  targetSocket.on('data', (chunk: Buffer) => {
948
1049
  connectionRecord.bytesSent += chunk.length;
949
1050
  this.updateActivity(connectionRecord);
950
1051
  });
951
-
1052
+
952
1053
  // Wait for the outgoing connection to be ready before setting up piping
953
1054
  targetSocket.once('connect', () => {
954
1055
  // Clear the initial connection error handler
955
1056
  targetSocket.removeAllListeners('error');
956
-
1057
+
957
1058
  // Add the normal error handler for established connections
958
1059
  targetSocket.on('error', handleError('outgoing'));
959
-
1060
+
960
1061
  // Remove temporary data handler
961
1062
  socket.removeListener('data', tempDataHandler);
962
-
1063
+
963
1064
  // Flush all pending data to target
964
1065
  if (connectionRecord.pendingData.length > 0) {
965
1066
  const combinedData = Buffer.concat(connectionRecord.pendingData);
966
1067
  targetSocket.write(combinedData, (err) => {
967
1068
  if (err) {
968
- console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
1069
+ console.log(
1070
+ `[${connectionId}] Error writing pending data to target: ${err.message}`
1071
+ );
969
1072
  return initiateCleanupOnce('write_error');
970
1073
  }
971
-
1074
+
972
1075
  // Now set up piping for future data and resume the socket
973
1076
  socket.pipe(targetSocket);
974
1077
  targetSocket.pipe(socket);
975
1078
  socket.resume(); // Resume the socket after piping is established
976
-
1079
+
977
1080
  if (this.settings.enableDetailedLogging) {
978
1081
  console.log(
979
1082
  `[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
980
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
981
- ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
1083
+ `${
1084
+ serverName
1085
+ ? ` (SNI: ${serverName})`
1086
+ : forcedDomain
1087
+ ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
1088
+ : ''
1089
+ }` +
1090
+ ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
982
1091
  );
983
1092
  } else {
984
1093
  console.log(
985
1094
  `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
986
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
1095
+ `${
1096
+ serverName
1097
+ ? ` (SNI: ${serverName})`
1098
+ : forcedDomain
1099
+ ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
1100
+ : ''
1101
+ }`
987
1102
  );
988
1103
  }
989
1104
  });
@@ -992,25 +1107,37 @@ export class PortProxy {
992
1107
  socket.pipe(targetSocket);
993
1108
  targetSocket.pipe(socket);
994
1109
  socket.resume(); // Resume the socket after piping is established
995
-
1110
+
996
1111
  if (this.settings.enableDetailedLogging) {
997
1112
  console.log(
998
1113
  `[${connectionId}] Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
999
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}` +
1000
- ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
1114
+ `${
1115
+ serverName
1116
+ ? ` (SNI: ${serverName})`
1117
+ : forcedDomain
1118
+ ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
1119
+ : ''
1120
+ }` +
1121
+ ` TLS: ${connectionRecord.isTLS ? 'Yes' : 'No'}`
1001
1122
  );
1002
1123
  } else {
1003
1124
  console.log(
1004
1125
  `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
1005
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
1126
+ `${
1127
+ serverName
1128
+ ? ` (SNI: ${serverName})`
1129
+ : forcedDomain
1130
+ ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})`
1131
+ : ''
1132
+ }`
1006
1133
  );
1007
1134
  }
1008
1135
  }
1009
-
1136
+
1010
1137
  // Clear the buffer now that we've processed it
1011
1138
  connectionRecord.pendingData = [];
1012
1139
  connectionRecord.pendingDataSize = 0;
1013
-
1140
+
1014
1141
  // Add the renegotiation listener for SNI validation
1015
1142
  if (serverName) {
1016
1143
  socket.on('data', (renegChunk: Buffer) => {
@@ -1019,41 +1146,53 @@ export class PortProxy {
1019
1146
  // Try to extract SNI from potential renegotiation
1020
1147
  const newSNI = extractSNI(renegChunk, this.settings.enableTlsDebugLogging);
1021
1148
  if (newSNI && newSNI !== connectionRecord.lockedDomain) {
1022
- console.log(`[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`);
1149
+ console.log(
1150
+ `[${connectionId}] Rehandshake detected with different SNI: ${newSNI} vs locked ${connectionRecord.lockedDomain}. Terminating connection.`
1151
+ );
1023
1152
  initiateCleanupOnce('sni_mismatch');
1024
1153
  } else if (newSNI && this.settings.enableDetailedLogging) {
1025
- console.log(`[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`);
1154
+ console.log(
1155
+ `[${connectionId}] Rehandshake detected with same SNI: ${newSNI}. Allowing.`
1156
+ );
1026
1157
  }
1027
1158
  } catch (err) {
1028
- console.log(`[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`);
1159
+ console.log(
1160
+ `[${connectionId}] Error processing potential renegotiation: ${err}. Allowing connection to continue.`
1161
+ );
1029
1162
  }
1030
1163
  }
1031
1164
  });
1032
1165
  }
1033
-
1166
+
1034
1167
  // Set connection timeout
1035
1168
  if (connectionRecord.cleanupTimer) {
1036
1169
  clearTimeout(connectionRecord.cleanupTimer);
1037
1170
  }
1038
-
1171
+
1039
1172
  // Set timeout based on domain config or default
1040
1173
  const connectionTimeout = this.getConnectionTimeout(connectionRecord);
1041
1174
  connectionRecord.cleanupTimer = setTimeout(() => {
1042
- console.log(`[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`);
1175
+ console.log(
1176
+ `[${connectionId}] Connection from ${remoteIP} exceeded max lifetime (${plugins.prettyMs(
1177
+ connectionTimeout
1178
+ )}), forcing cleanup.`
1179
+ );
1043
1180
  initiateCleanupOnce('connection_timeout');
1044
1181
  }, connectionTimeout);
1045
-
1182
+
1046
1183
  // Make sure timeout doesn't keep the process alive
1047
1184
  if (connectionRecord.cleanupTimer.unref) {
1048
1185
  connectionRecord.cleanupTimer.unref();
1049
1186
  }
1050
-
1187
+
1051
1188
  // Mark TLS handshake as complete for TLS connections
1052
1189
  if (connectionRecord.isTLS) {
1053
1190
  connectionRecord.tlsHandshakeComplete = true;
1054
-
1191
+
1055
1192
  if (this.settings.enableTlsDebugLogging) {
1056
- console.log(`[${connectionId}] TLS handshake complete for connection from ${remoteIP}`);
1193
+ console.log(
1194
+ `[${connectionId}] TLS handshake complete for connection from ${remoteIP}`
1195
+ );
1057
1196
  }
1058
1197
  }
1059
1198
  });
@@ -1061,45 +1200,72 @@ export class PortProxy {
1061
1200
 
1062
1201
  // --- PORT RANGE-BASED HANDLING ---
1063
1202
  // Only apply port-based rules if the incoming port is within one of the global port ranges.
1064
- if (this.settings.globalPortRanges && isPortInRanges(localPort, this.settings.globalPortRanges)) {
1203
+ if (
1204
+ this.settings.globalPortRanges &&
1205
+ isPortInRanges(localPort, this.settings.globalPortRanges)
1206
+ ) {
1065
1207
  if (this.settings.forwardAllGlobalRanges) {
1066
- if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
1067
- console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
1208
+ if (
1209
+ this.settings.defaultAllowedIPs &&
1210
+ this.settings.defaultAllowedIPs.length > 0 &&
1211
+ !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
1212
+ ) {
1213
+ console.log(
1214
+ `[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`
1215
+ );
1068
1216
  socket.end();
1069
1217
  return;
1070
1218
  }
1071
1219
  if (this.settings.enableDetailedLogging) {
1072
- console.log(`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
1220
+ console.log(
1221
+ `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
1222
+ );
1073
1223
  }
1074
- setupConnection('', undefined, {
1075
- domains: ['global'],
1076
- allowedIPs: this.settings.defaultAllowedIPs || [],
1077
- blockedIPs: this.settings.defaultBlockedIPs || [],
1078
- targetIPs: [this.settings.targetIP!],
1079
- portRanges: []
1080
- }, localPort);
1224
+ setupConnection(
1225
+ '',
1226
+ undefined,
1227
+ {
1228
+ domains: ['global'],
1229
+ allowedIPs: this.settings.defaultAllowedIPs || [],
1230
+ blockedIPs: this.settings.defaultBlockedIPs || [],
1231
+ targetIPs: [this.settings.targetIP!],
1232
+ portRanges: [],
1233
+ },
1234
+ localPort
1235
+ );
1081
1236
  return;
1082
1237
  } else {
1083
1238
  // Attempt to find a matching forced domain config based on the local port.
1084
1239
  const forcedDomain = this.settings.domainConfigs.find(
1085
- domain => domain.portRanges && domain.portRanges.length > 0 && isPortInRanges(localPort, domain.portRanges)
1240
+ (domain) =>
1241
+ domain.portRanges &&
1242
+ domain.portRanges.length > 0 &&
1243
+ isPortInRanges(localPort, domain.portRanges)
1086
1244
  );
1087
1245
  if (forcedDomain) {
1088
1246
  const effectiveAllowedIPs: string[] = [
1089
1247
  ...forcedDomain.allowedIPs,
1090
- ...(this.settings.defaultAllowedIPs || [])
1248
+ ...(this.settings.defaultAllowedIPs || []),
1091
1249
  ];
1092
1250
  const effectiveBlockedIPs: string[] = [
1093
1251
  ...(forcedDomain.blockedIPs || []),
1094
- ...(this.settings.defaultBlockedIPs || [])
1252
+ ...(this.settings.defaultBlockedIPs || []),
1095
1253
  ];
1096
1254
  if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
1097
- console.log(`[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
1255
+ console.log(
1256
+ `[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
1257
+ ', '
1258
+ )} on port ${localPort}.`
1259
+ );
1098
1260
  socket.end();
1099
1261
  return;
1100
1262
  }
1101
1263
  if (this.settings.enableDetailedLogging) {
1102
- console.log(`[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
1264
+ console.log(
1265
+ `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
1266
+ ', '
1267
+ )}.`
1268
+ );
1103
1269
  }
1104
1270
  setupConnection('', undefined, forcedDomain, localPort);
1105
1271
  return;
@@ -1117,39 +1283,52 @@ export class PortProxy {
1117
1283
  clearTimeout(initialTimeout);
1118
1284
  initialTimeout = null;
1119
1285
  }
1120
-
1286
+
1121
1287
  initialDataReceived = true;
1122
-
1288
+
1123
1289
  // Try to extract SNI
1124
1290
  let serverName = '';
1125
-
1291
+
1126
1292
  if (isTlsHandshake(chunk)) {
1127
1293
  connectionRecord.isTLS = true;
1128
-
1294
+
1129
1295
  if (this.settings.enableTlsDebugLogging) {
1130
- console.log(`[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`);
1296
+ console.log(
1297
+ `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
1298
+ );
1131
1299
  }
1132
-
1300
+
1133
1301
  serverName = extractSNI(chunk, this.settings.enableTlsDebugLogging) || '';
1134
1302
  }
1135
-
1303
+
1136
1304
  // Lock the connection to the negotiated SNI.
1137
1305
  connectionRecord.lockedDomain = serverName;
1138
-
1306
+
1139
1307
  if (this.settings.enableDetailedLogging) {
1140
- console.log(`[${connectionId}] Received connection from ${remoteIP} with SNI: ${serverName || '(empty)'}`);
1308
+ console.log(
1309
+ `[${connectionId}] Received connection from ${remoteIP} with SNI: ${
1310
+ serverName || '(empty)'
1311
+ }`
1312
+ );
1141
1313
  }
1142
-
1314
+
1143
1315
  setupConnection(serverName, chunk);
1144
1316
  });
1145
1317
  } else {
1146
1318
  initialDataReceived = true;
1147
1319
  connectionRecord.hasReceivedInitialData = true;
1148
-
1149
- if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0 && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
1150
- return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
1320
+
1321
+ if (
1322
+ this.settings.defaultAllowedIPs &&
1323
+ this.settings.defaultAllowedIPs.length > 0 &&
1324
+ !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
1325
+ ) {
1326
+ return rejectIncomingConnection(
1327
+ 'rejected',
1328
+ `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`
1329
+ );
1151
1330
  }
1152
-
1331
+
1153
1332
  setupConnection('');
1154
1333
  }
1155
1334
  };
@@ -1172,13 +1351,15 @@ export class PortProxy {
1172
1351
 
1173
1352
  // Create a server for each port.
1174
1353
  for (const port of listeningPorts) {
1175
- const server = plugins.net
1176
- .createServer(connectionHandler)
1177
- .on('error', (err: Error) => {
1178
- console.log(`Server Error on port ${port}: ${err.message}`);
1179
- });
1354
+ const server = plugins.net.createServer(connectionHandler).on('error', (err: Error) => {
1355
+ console.log(`Server Error on port ${port}: ${err.message}`);
1356
+ });
1180
1357
  server.listen(port, () => {
1181
- console.log(`PortProxy -> OK: Now listening on port ${port}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
1358
+ console.log(
1359
+ `PortProxy -> OK: Now listening on port ${port}${
1360
+ this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''
1361
+ }`
1362
+ );
1182
1363
  });
1183
1364
  this.netServers.push(server);
1184
1365
  }
@@ -1187,7 +1368,7 @@ export class PortProxy {
1187
1368
  this.connectionLogger = setInterval(() => {
1188
1369
  // Immediately return if shutting down
1189
1370
  if (this.isShuttingDown) return;
1190
-
1371
+
1191
1372
  const now = Date.now();
1192
1373
  let maxIncoming = 0;
1193
1374
  let maxOutgoing = 0;
@@ -1195,14 +1376,14 @@ export class PortProxy {
1195
1376
  let nonTlsConnections = 0;
1196
1377
  let completedTlsHandshakes = 0;
1197
1378
  let pendingTlsHandshakes = 0;
1198
-
1379
+
1199
1380
  // Create a copy of the keys to avoid modification during iteration
1200
1381
  const connectionIds = [...this.connectionRecords.keys()];
1201
-
1382
+
1202
1383
  for (const id of connectionIds) {
1203
1384
  const record = this.connectionRecords.get(id);
1204
1385
  if (!record) continue;
1205
-
1386
+
1206
1387
  // Track connection stats
1207
1388
  if (record.isTLS) {
1208
1389
  tlsConnections++;
@@ -1214,50 +1395,73 @@ export class PortProxy {
1214
1395
  } else {
1215
1396
  nonTlsConnections++;
1216
1397
  }
1217
-
1398
+
1218
1399
  maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
1219
1400
  if (record.outgoingStartTime) {
1220
1401
  maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
1221
1402
  }
1222
-
1403
+
1223
1404
  // Parity check: if outgoing socket closed and incoming remains active
1224
- if (record.outgoingClosedTime &&
1225
- !record.incoming.destroyed &&
1226
- !record.connectionClosed &&
1227
- (now - record.outgoingClosedTime > 120000)) {
1405
+ if (
1406
+ record.outgoingClosedTime &&
1407
+ !record.incoming.destroyed &&
1408
+ !record.connectionClosed &&
1409
+ now - record.outgoingClosedTime > 120000
1410
+ ) {
1228
1411
  const remoteIP = record.remoteIP;
1229
- console.log(`[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`);
1412
+ console.log(
1413
+ `[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(
1414
+ now - record.outgoingClosedTime
1415
+ )} after outgoing closed.`
1416
+ );
1230
1417
  this.cleanupConnection(record, 'parity_check');
1231
1418
  }
1232
-
1419
+
1233
1420
  // Check for stalled connections waiting for initial data
1234
- if (!record.hasReceivedInitialData &&
1235
- (now - record.incomingStartTime > this.settings.initialDataTimeout! / 2)) {
1236
- console.log(`[${id}] Warning: Connection from ${record.remoteIP} has not received initial data after ${plugins.prettyMs(now - record.incomingStartTime)}`);
1421
+ if (
1422
+ !record.hasReceivedInitialData &&
1423
+ now - record.incomingStartTime > this.settings.initialDataTimeout! / 2
1424
+ ) {
1425
+ console.log(
1426
+ `[${id}] Warning: Connection from ${
1427
+ record.remoteIP
1428
+ } has not received initial data after ${plugins.prettyMs(
1429
+ now - record.incomingStartTime
1430
+ )}`
1431
+ );
1237
1432
  }
1238
-
1433
+
1239
1434
  // Skip inactivity check if disabled
1240
1435
  if (!this.settings.disableInactivityCheck) {
1241
1436
  // Inactivity check with configurable timeout
1242
1437
  const inactivityThreshold = this.settings.inactivityTimeout!;
1243
-
1438
+
1244
1439
  const inactivityTime = now - record.lastActivity;
1245
1440
  if (inactivityTime > inactivityThreshold && !record.connectionClosed) {
1246
- console.log(`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
1441
+ console.log(
1442
+ `[${id}] Inactivity check: No activity on connection from ${
1443
+ record.remoteIP
1444
+ } for ${plugins.prettyMs(inactivityTime)}.`
1445
+ );
1247
1446
  this.cleanupConnection(record, 'inactivity');
1248
1447
  }
1249
1448
  }
1250
1449
  }
1251
-
1450
+
1252
1451
  // Log detailed stats periodically
1253
1452
  console.log(
1254
1453
  `Active connections: ${this.connectionRecords.size}. ` +
1255
- `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` +
1256
- `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` +
1257
- `Termination stats: ${JSON.stringify({IN: this.terminationStats.incoming, OUT: this.terminationStats.outgoing})}`
1454
+ `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), Non-TLS=${nonTlsConnections}. ` +
1455
+ `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
1456
+ maxOutgoing
1457
+ )}. ` +
1458
+ `Termination stats: ${JSON.stringify({
1459
+ IN: this.terminationStats.incoming,
1460
+ OUT: this.terminationStats.outgoing,
1461
+ })}`
1258
1462
  );
1259
1463
  }, this.settings.inactivityCheckInterval || 60000);
1260
-
1464
+
1261
1465
  // Make sure the interval doesn't keep the process alive
1262
1466
  if (this.connectionLogger.unref) {
1263
1467
  this.connectionLogger.unref();
@@ -1268,12 +1472,12 @@ export class PortProxy {
1268
1472
  * Gracefully shut down the proxy
1269
1473
  */
1270
1474
  public async stop() {
1271
- console.log("PortProxy shutting down...");
1475
+ console.log('PortProxy shutting down...');
1272
1476
  this.isShuttingDown = true;
1273
-
1477
+
1274
1478
  // Stop accepting new connections
1275
1479
  const closeServerPromises: Promise<void>[] = this.netServers.map(
1276
- server =>
1480
+ (server) =>
1277
1481
  new Promise<void>((resolve) => {
1278
1482
  if (!server.listening) {
1279
1483
  resolve();
@@ -1287,7 +1491,7 @@ export class PortProxy {
1287
1491
  });
1288
1492
  })
1289
1493
  );
1290
-
1494
+
1291
1495
  // Stop the connection logger
1292
1496
  if (this.connectionLogger) {
1293
1497
  clearInterval(this.connectionLogger);
@@ -1296,12 +1500,12 @@ export class PortProxy {
1296
1500
 
1297
1501
  // Wait for servers to close
1298
1502
  await Promise.all(closeServerPromises);
1299
- console.log("All servers closed. Cleaning up active connections...");
1300
-
1503
+ console.log('All servers closed. Cleaning up active connections...');
1504
+
1301
1505
  // Force destroy all active connections immediately
1302
1506
  const connectionIds = [...this.connectionRecords.keys()];
1303
1507
  console.log(`Cleaning up ${connectionIds.length} active connections...`);
1304
-
1508
+
1305
1509
  // First pass: End all connections gracefully
1306
1510
  for (const id of connectionIds) {
1307
1511
  const record = this.connectionRecords.get(id);
@@ -1312,12 +1516,12 @@ export class PortProxy {
1312
1516
  clearTimeout(record.cleanupTimer);
1313
1517
  record.cleanupTimer = undefined;
1314
1518
  }
1315
-
1519
+
1316
1520
  // End sockets gracefully
1317
1521
  if (record.incoming && !record.incoming.destroyed) {
1318
1522
  record.incoming.end();
1319
1523
  }
1320
-
1524
+
1321
1525
  if (record.outgoing && !record.outgoing.destroyed) {
1322
1526
  record.outgoing.end();
1323
1527
  }
@@ -1326,10 +1530,10 @@ export class PortProxy {
1326
1530
  }
1327
1531
  }
1328
1532
  }
1329
-
1533
+
1330
1534
  // Short delay to allow graceful ends to process
1331
- await new Promise(resolve => setTimeout(resolve, 100));
1332
-
1535
+ await new Promise((resolve) => setTimeout(resolve, 100));
1536
+
1333
1537
  // Second pass: Force destroy everything
1334
1538
  for (const id of connectionIds) {
1335
1539
  const record = this.connectionRecords.get(id);
@@ -1342,7 +1546,7 @@ export class PortProxy {
1342
1546
  record.incoming.destroy();
1343
1547
  }
1344
1548
  }
1345
-
1549
+
1346
1550
  if (record.outgoing) {
1347
1551
  record.outgoing.removeAllListeners();
1348
1552
  if (!record.outgoing.destroyed) {
@@ -1354,20 +1558,20 @@ export class PortProxy {
1354
1558
  }
1355
1559
  }
1356
1560
  }
1357
-
1561
+
1358
1562
  // Clear all tracking maps
1359
1563
  this.connectionRecords.clear();
1360
1564
  this.domainTargetIndices.clear();
1361
1565
  this.connectionsByIP.clear();
1362
1566
  this.connectionRateByIP.clear();
1363
1567
  this.netServers = [];
1364
-
1568
+
1365
1569
  // Reset termination stats
1366
1570
  this.terminationStats = {
1367
1571
  incoming: {},
1368
- outgoing: {}
1572
+ outgoing: {},
1369
1573
  };
1370
-
1371
- console.log("PortProxy shutdown complete.");
1574
+
1575
+ console.log('PortProxy shutdown complete.');
1372
1576
  }
1373
- }
1577
+ }