@push.rocks/smartproxy 3.22.1 → 3.22.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartproxy",
3
- "version": "3.22.1",
3
+ "version": "3.22.3",
4
4
  "private": false,
5
5
  "description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
6
6
  "main": "dist_ts/index.js",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '3.22.1',
6
+ version: '3.22.3',
7
7
  description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
8
8
  }
@@ -23,7 +23,6 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
23
23
  globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
24
24
  forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
25
25
  gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
26
- initialDataTimeout?: number; // (ms) timeout for receiving initial data, useful for chained proxies
27
26
  }
28
27
 
29
28
  /**
@@ -90,25 +89,24 @@ function extractSNI(buffer: Buffer): string | undefined {
90
89
  }
91
90
 
92
91
  interface IConnectionRecord {
92
+ id: string; // Unique connection identifier
93
93
  incoming: plugins.net.Socket;
94
94
  outgoing: plugins.net.Socket | null;
95
95
  incomingStartTime: number;
96
96
  outgoingStartTime?: number;
97
97
  outgoingClosedTime?: number;
98
- lockedDomain?: string; // Field to lock this connection to the initial SNI
99
- connectionClosed: boolean;
100
- cleanupTimer?: NodeJS.Timeout; // Timer to force cleanup after max lifetime/inactivity
101
- cleanupInitiated: boolean; // Flag to track if cleanup has been initiated but not completed
102
- id: string; // Unique identifier for the connection
103
- lastActivity: number; // Timestamp of last activity on either socket
98
+ lockedDomain?: string; // Used to lock this connection to the initial SNI
99
+ connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
100
+ cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
101
+ lastActivity: number; // Last activity timestamp for inactivity detection
104
102
  }
105
103
 
106
- // Helper: Check if a port falls within any of the given port ranges.
104
+ // Helper: Check if a port falls within any of the given port ranges
107
105
  const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
108
106
  return ranges.some(range => port >= range.from && port <= range.to);
109
107
  };
110
108
 
111
- // Helper: Check if a given IP matches any of the glob patterns.
109
+ // Helper: Check if a given IP matches any of the glob patterns
112
110
  const isAllowed = (ip: string, patterns: string[]): boolean => {
113
111
  const normalizeIP = (ip: string): string[] => {
114
112
  if (ip.startsWith('::ffff:')) {
@@ -127,13 +125,13 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
127
125
  );
128
126
  };
129
127
 
130
- // Helper: Check if an IP is allowed considering allowed and blocked glob patterns.
128
+ // Helper: Check if an IP is allowed considering allowed and blocked glob patterns
131
129
  const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
132
130
  if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
133
131
  return isAllowed(ip, allowed);
134
132
  };
135
133
 
136
- // Helper: Generate a unique ID for a connection
134
+ // Helper: Generate a unique connection ID
137
135
  const generateConnectionId = (): string => {
138
136
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
139
137
  };
@@ -141,12 +139,11 @@ const generateConnectionId = (): string => {
141
139
  export class PortProxy {
142
140
  private netServers: plugins.net.Server[] = [];
143
141
  settings: IPortProxySettings;
144
- // Unified record tracking each connection pair.
145
142
  private connectionRecords: Map<string, IConnectionRecord> = new Map();
146
143
  private connectionLogger: NodeJS.Timeout | null = null;
147
144
  private isShuttingDown: boolean = false;
148
145
 
149
- // Map to track round robin indices for each domain config.
146
+ // Map to track round robin indices for each domain config
150
147
  private domainTargetIndices: Map<IDomainConfig, number> = new Map();
151
148
 
152
149
  private terminationStats: {
@@ -164,9 +161,6 @@ export class PortProxy {
164
161
  maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
165
162
  gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
166
163
  };
167
-
168
- // Debug logging for constructor settings
169
- console.log(`PortProxy initialized with targetIP: ${this.settings.targetIP}, toPort: ${this.settings.toPort}, fromPort: ${this.settings.fromPort}, sniEnabled: ${this.settings.sniEnabled}`);
170
164
  }
171
165
 
172
166
  private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
@@ -174,77 +168,64 @@ export class PortProxy {
174
168
  }
175
169
 
176
170
  /**
177
- * Initiates the cleanup process for a connection.
178
- * Sets the flag to prevent duplicate cleanup attempts and schedules actual cleanup.
179
- */
180
- private initiateCleanup(record: IConnectionRecord, reason: string = 'normal'): void {
181
- if (record.cleanupInitiated) return;
182
-
183
- record.cleanupInitiated = true;
184
- const remoteIP = record.incoming.remoteAddress || 'unknown';
185
- console.log(`Initiating cleanup for connection ${record.id} from ${remoteIP} (reason: ${reason})`);
186
-
187
- // Execute cleanup immediately to prevent lingering connections
188
- this.executeCleanup(record);
189
- }
190
-
191
- /**
192
- * Executes the actual cleanup of a connection.
193
- * Destroys sockets, clears timers, and removes the record.
171
+ * Cleans up a connection record.
172
+ * Destroys both incoming and outgoing sockets, clears timers, and removes the record.
173
+ * @param record - The connection record to clean up
174
+ * @param reason - Optional reason for cleanup (for logging)
194
175
  */
195
- private executeCleanup(record: IConnectionRecord): void {
196
- if (record.connectionClosed) return;
197
-
198
- record.connectionClosed = true;
199
- const remoteIP = record.incoming.remoteAddress || 'unknown';
200
-
201
- if (record.cleanupTimer) {
202
- clearTimeout(record.cleanupTimer);
203
- record.cleanupTimer = undefined;
204
- }
205
-
206
- // End the sockets first to allow for graceful closure
207
- try {
208
- if (!record.incoming.destroyed) {
209
- record.incoming.end();
210
- // Set a safety timeout to force destroy if end doesn't complete
211
- setTimeout(() => {
212
- if (!record.incoming.destroyed) {
213
- console.log(`Forcing destruction of incoming socket for ${remoteIP}`);
214
- record.incoming.destroy();
215
- }
216
- }, 1000);
217
- }
218
- } catch (err) {
219
- console.error(`Error ending incoming socket for ${remoteIP}:`, err);
220
- if (!record.incoming.destroyed) {
221
- record.incoming.destroy();
176
+ private cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
177
+ if (!record.connectionClosed) {
178
+ record.connectionClosed = true;
179
+
180
+ if (record.cleanupTimer) {
181
+ clearTimeout(record.cleanupTimer);
182
+ record.cleanupTimer = undefined;
222
183
  }
223
- }
224
-
225
- try {
226
- if (record.outgoing && !record.outgoing.destroyed) {
227
- record.outgoing.end();
228
- // Set a safety timeout to force destroy if end doesn't complete
229
- setTimeout(() => {
230
- if (record.outgoing && !record.outgoing.destroyed) {
231
- console.log(`Forcing destruction of outgoing socket for ${remoteIP}`);
232
- record.outgoing.destroy();
233
- }
234
- }, 1000);
184
+
185
+ try {
186
+ if (!record.incoming.destroyed) {
187
+ // Try graceful shutdown first, then force destroy after a short timeout
188
+ record.incoming.end();
189
+ setTimeout(() => {
190
+ if (record && !record.incoming.destroyed) {
191
+ record.incoming.destroy();
192
+ }
193
+ }, 1000);
194
+ }
195
+ } catch (err) {
196
+ console.log(`Error closing incoming socket: ${err}`);
197
+ if (!record.incoming.destroyed) {
198
+ record.incoming.destroy();
199
+ }
235
200
  }
236
- } catch (err) {
237
- console.error(`Error ending outgoing socket for ${remoteIP}:`, err);
238
- if (record.outgoing && !record.outgoing.destroyed) {
239
- record.outgoing.destroy();
201
+
202
+ try {
203
+ if (record.outgoing && !record.outgoing.destroyed) {
204
+ // Try graceful shutdown first, then force destroy after a short timeout
205
+ record.outgoing.end();
206
+ setTimeout(() => {
207
+ if (record && record.outgoing && !record.outgoing.destroyed) {
208
+ record.outgoing.destroy();
209
+ }
210
+ }, 1000);
211
+ }
212
+ } catch (err) {
213
+ console.log(`Error closing outgoing socket: ${err}`);
214
+ if (record.outgoing && !record.outgoing.destroyed) {
215
+ record.outgoing.destroy();
216
+ }
240
217
  }
218
+
219
+ // Remove the record from the tracking map
220
+ this.connectionRecords.delete(record.id);
221
+
222
+ const remoteIP = record.incoming.remoteAddress || 'unknown';
223
+ console.log(`Connection from ${remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`);
241
224
  }
225
+ }
242
226
 
243
- // Remove the record after a delay to ensure all events have propagated
244
- setTimeout(() => {
245
- this.connectionRecords.delete(record.id);
246
- console.log(`Connection ${record.id} from ${remoteIP} fully cleaned up. Active connections: ${this.connectionRecords.size}`);
247
- }, 2000);
227
+ private updateActivity(record: IConnectionRecord): void {
228
+ record.lastActivity = Date.now();
248
229
  }
249
230
 
250
231
  private getTargetIP(domainConfig: IDomainConfig): string {
@@ -257,27 +238,6 @@ export class PortProxy {
257
238
  return this.settings.targetIP!;
258
239
  }
259
240
 
260
- /**
261
- * Updates the last activity timestamp for a connection record
262
- */
263
- private updateActivity(record: IConnectionRecord): void {
264
- record.lastActivity = Date.now();
265
-
266
- // Reset the inactivity timer if one is set
267
- if (this.settings.maxConnectionLifetime && record.cleanupTimer) {
268
- clearTimeout(record.cleanupTimer);
269
-
270
- // Set a new cleanup timer
271
- record.cleanupTimer = setTimeout(() => {
272
- const now = Date.now();
273
- const inactivityTime = now - record.lastActivity;
274
- const remoteIP = record.incoming.remoteAddress || 'unknown';
275
- console.log(`Connection ${record.id} from ${remoteIP} exceeded max lifetime or inactivity period (${inactivityTime}ms), forcing cleanup.`);
276
- this.initiateCleanup(record, 'timeout');
277
- }, this.settings.maxConnectionLifetime);
278
- }
279
- }
280
-
281
241
  public async start() {
282
242
  // Define a unified connection handler for all listening ports.
283
243
  const connectionHandler = (socket: plugins.net.Socket) => {
@@ -297,23 +257,28 @@ export class PortProxy {
297
257
  outgoing: null,
298
258
  incomingStartTime: Date.now(),
299
259
  lastActivity: Date.now(),
300
- connectionClosed: false,
301
- cleanupInitiated: false
260
+ connectionClosed: false
302
261
  };
303
-
304
262
  this.connectionRecords.set(connectionId, connectionRecord);
305
- console.log(`New connection ${connectionId} from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
263
+
264
+ console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
306
265
 
307
266
  let initialDataReceived = false;
308
267
  let incomingTerminationReason: string | null = null;
309
268
  let outgoingTerminationReason: string | null = null;
310
269
 
311
- // Local cleanup function that delegates to the class method.
270
+ // Local function for cleanupOnce
271
+ const cleanupOnce = () => {
272
+ this.cleanupConnection(connectionRecord);
273
+ };
274
+
275
+ // Define initiateCleanupOnce for compatibility with potential future improvements
312
276
  const initiateCleanupOnce = (reason: string = 'normal') => {
313
- this.initiateCleanup(connectionRecord, reason);
277
+ console.log(`Connection cleanup initiated for ${remoteIP} (${reason})`);
278
+ cleanupOnce();
314
279
  };
315
280
 
316
- // Helper to reject an incoming connection.
281
+ // Helper to reject an incoming connection
317
282
  const rejectIncomingConnection = (reason: string, logMessage: string) => {
318
283
  console.log(logMessage);
319
284
  socket.end();
@@ -321,43 +286,17 @@ export class PortProxy {
321
286
  incomingTerminationReason = reason;
322
287
  this.incrementTerminationStat('incoming', reason);
323
288
  }
324
- initiateCleanupOnce(reason);
289
+ cleanupOnce();
325
290
  };
326
291
 
327
- // IMPORTANT: We won't set any initial timeout for a chained proxy scenario
328
- // The code below is commented out to restore original behavior
329
- /*
330
- let initialTimeout: NodeJS.Timeout | null = null;
331
- const initialTimeoutMs = this.settings.initialDataTimeout ||
332
- (this.settings.sniEnabled ? 15000 : 0);
333
-
334
- if (initialTimeoutMs > 0) {
335
- console.log(`Setting initial data timeout of ${initialTimeoutMs}ms for connection from ${remoteIP}`);
336
- initialTimeout = setTimeout(() => {
337
- if (!initialDataReceived) {
338
- console.log(`Initial connection timeout for ${remoteIP} (no data received after ${initialTimeoutMs}ms)`);
339
- if (incomingTerminationReason === null) {
340
- incomingTerminationReason = 'initial_timeout';
341
- this.incrementTerminationStat('incoming', 'initial_timeout');
342
- }
343
- initiateCleanupOnce('initial_timeout');
344
- }
345
- }, initialTimeoutMs);
346
- } else {
347
- console.log(`No initial timeout set for connection from ${remoteIP} (likely chained proxy)`);
348
- initialDataReceived = true;
349
- }
350
- */
351
-
352
- // Original behavior: only set timeout if SNI is enabled, and use a fixed 5 second timeout
292
+ // Set an initial timeout for SNI data if needed
353
293
  let initialTimeout: NodeJS.Timeout | null = null;
354
294
  if (this.settings.sniEnabled) {
355
- console.log(`Setting 5 second initial timeout for SNI extraction from ${remoteIP}`);
356
295
  initialTimeout = setTimeout(() => {
357
296
  if (!initialDataReceived) {
358
297
  console.log(`Initial data timeout for ${remoteIP}`);
359
298
  socket.end();
360
- initiateCleanupOnce('initial_timeout');
299
+ cleanupOnce();
361
300
  }
362
301
  }, 5000);
363
302
  } else {
@@ -365,26 +304,7 @@ export class PortProxy {
365
304
  }
366
305
 
367
306
  socket.on('error', (err: Error) => {
368
- const errorMessage = initialDataReceived
369
- ? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
370
- : `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
371
- console.log(errorMessage);
372
-
373
- // Clear the initial timeout if it exists
374
- if (initialTimeout) {
375
- clearTimeout(initialTimeout);
376
- initialTimeout = null;
377
- }
378
-
379
- // For premature errors, we need to handle them explicitly
380
- // since the standard error handlers might not be set up yet
381
- if (!initialDataReceived) {
382
- if (incomingTerminationReason === null) {
383
- incomingTerminationReason = 'premature_error';
384
- this.incrementTerminationStat('incoming', 'premature_error');
385
- }
386
- initiateCleanupOnce('premature_error');
387
- }
307
+ console.log(`Incoming socket error from ${remoteIP}: ${err.message}`);
388
308
  });
389
309
 
390
310
  const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
@@ -393,13 +313,9 @@ export class PortProxy {
393
313
  if (code === 'ECONNRESET') {
394
314
  reason = 'econnreset';
395
315
  console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
396
- } else if (code === 'ECONNREFUSED') {
397
- reason = 'econnrefused';
398
- console.log(`ECONNREFUSED on ${side} side from ${remoteIP}: ${err.message}`);
399
316
  } else {
400
317
  console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
401
318
  }
402
-
403
319
  if (side === 'incoming' && incomingTerminationReason === null) {
404
320
  incomingTerminationReason = reason;
405
321
  this.incrementTerminationStat('incoming', reason);
@@ -407,13 +323,11 @@ export class PortProxy {
407
323
  outgoingTerminationReason = reason;
408
324
  this.incrementTerminationStat('outgoing', reason);
409
325
  }
410
-
411
326
  initiateCleanupOnce(reason);
412
327
  };
413
328
 
414
329
  const handleClose = (side: 'incoming' | 'outgoing') => () => {
415
330
  console.log(`Connection closed on ${side} side from ${remoteIP}`);
416
-
417
331
  if (side === 'incoming' && incomingTerminationReason === null) {
418
332
  incomingTerminationReason = 'normal';
419
333
  this.incrementTerminationStat('incoming', 'normal');
@@ -422,24 +336,8 @@ export class PortProxy {
422
336
  this.incrementTerminationStat('outgoing', 'normal');
423
337
  // Record the time when outgoing socket closed.
424
338
  connectionRecord.outgoingClosedTime = Date.now();
425
-
426
- // If incoming is still active but outgoing closed, set a shorter timeout
427
- if (!connectionRecord.incoming.destroyed) {
428
- console.log(`Outgoing socket closed but incoming still active for ${remoteIP}. Setting cleanup timeout.`);
429
- setTimeout(() => {
430
- if (!connectionRecord.connectionClosed && !connectionRecord.incoming.destroyed) {
431
- console.log(`Incoming socket still active ${Date.now() - connectionRecord.outgoingClosedTime!}ms after outgoing closed for ${remoteIP}. Cleaning up.`);
432
- initiateCleanupOnce('outgoing_closed_timeout');
433
- }
434
- }, 10000); // 10 second timeout instead of waiting for the next parity check
435
- }
436
- }
437
-
438
- // If both sides are closed/destroyed, clean up
439
- if ((side === 'incoming' && connectionRecord.outgoing?.destroyed) ||
440
- (side === 'outgoing' && connectionRecord.incoming.destroyed)) {
441
- initiateCleanupOnce('both_closed');
442
339
  }
340
+ initiateCleanupOnce('closed_' + side);
443
341
  };
444
342
 
445
343
  /**
@@ -453,6 +351,7 @@ export class PortProxy {
453
351
  // Clear the initial timeout since we've received data
454
352
  if (initialTimeout) {
455
353
  clearTimeout(initialTimeout);
354
+ initialTimeout = null;
456
355
  }
457
356
 
458
357
  // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
@@ -462,9 +361,7 @@ export class PortProxy {
462
361
  config.domains.some(d => plugins.minimatch(serverName, d))
463
362
  ) : undefined);
464
363
 
465
- // Effective IP check: merge allowed IPs with default allowed, and remove blocked IPs.
466
- // Use original domain configuration and IP validation logic
467
- // This restores the behavior that was working before
364
+ // IP validation is skipped if allowedIPs is empty
468
365
  if (domainConfig) {
469
366
  const effectiveAllowedIPs: string[] = [
470
367
  ...domainConfig.allowedIPs,
@@ -475,7 +372,7 @@ export class PortProxy {
475
372
  ...(this.settings.defaultBlockedIPs || [])
476
373
  ];
477
374
 
478
- // Special case: if allowedIPs is empty, skip IP validation for backward compatibility
375
+ // Skip IP validation if allowedIPs is empty
479
376
  if (domainConfig.allowedIPs.length > 0 && !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
480
377
  return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
481
378
  }
@@ -483,8 +380,7 @@ export class PortProxy {
483
380
  if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
484
381
  return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
485
382
  }
486
- }
487
- // If no IP validation rules, allow the connection (original behavior)
383
+ }
488
384
 
489
385
  const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
490
386
  const connectionOptions: plugins.net.NetConnectOpts = {
@@ -495,116 +391,57 @@ export class PortProxy {
495
391
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
496
392
  }
497
393
 
498
- // Add explicit connection timeout and error handling
499
- let connectionTimeout: NodeJS.Timeout | null = null;
500
- let connectionSucceeded = false;
501
-
502
- // Set connection timeout - longer for chained proxies
503
- connectionTimeout = setTimeout(() => {
504
- if (!connectionSucceeded) {
505
- console.log(`Connection timeout connecting to ${targetHost}:${connectionOptions.port} for ${remoteIP}`);
506
- if (outgoingTerminationReason === null) {
507
- outgoingTerminationReason = 'connection_timeout';
508
- this.incrementTerminationStat('outgoing', 'connection_timeout');
509
- }
510
- initiateCleanupOnce('connection_timeout');
511
- }
512
- }, 10000); // Increased from 5s to 10s to accommodate chained proxies
513
-
514
- console.log(`Attempting to connect to ${targetHost}:${connectionOptions.port} for client ${remoteIP}...`);
515
-
516
- // Create the target socket
394
+ // Create the target socket and immediately set up data piping
517
395
  const targetSocket = plugins.net.connect(connectionOptions);
518
396
  connectionRecord.outgoing = targetSocket;
397
+ connectionRecord.outgoingStartTime = Date.now();
519
398
 
520
- // Handle successful connection
521
- targetSocket.once('connect', () => {
522
- connectionSucceeded = true;
523
- if (connectionTimeout) {
524
- clearTimeout(connectionTimeout);
525
- connectionTimeout = null;
526
- }
527
-
528
- connectionRecord.outgoingStartTime = Date.now();
529
- console.log(
530
- `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
531
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
532
- );
533
-
534
- // Setup data flow after confirmed connection
535
- setupDataFlow(targetSocket, initialChunk);
536
- });
537
-
538
- // Handle connection errors early
539
- targetSocket.once('error', (err) => {
540
- if (!connectionSucceeded) {
541
- // This is an initial connection error
542
- console.log(`Failed to connect to ${targetHost}:${connectionOptions.port} for ${remoteIP}: ${err.message}`);
543
- if (connectionTimeout) {
544
- clearTimeout(connectionTimeout);
545
- connectionTimeout = null;
546
- }
547
- if (outgoingTerminationReason === null) {
548
- outgoingTerminationReason = 'connection_failed';
549
- this.incrementTerminationStat('outgoing', 'connection_failed');
550
- }
551
- initiateCleanupOnce('connection_failed');
552
- }
553
- // Other errors will be handled by the main error handler
554
- });
555
- };
556
-
557
- /**
558
- * Sets up the data flow between sockets after successful connection
559
- */
560
- const setupDataFlow = (targetSocket: plugins.net.Socket, initialChunk?: Buffer) => {
399
+ // Set up the pipe immediately to ensure data flows without delay
561
400
  if (initialChunk) {
562
401
  socket.unshift(initialChunk);
563
402
  }
564
403
 
565
- // Set appropriate timeouts for both sockets
566
- socket.setTimeout(120000);
567
- targetSocket.setTimeout(120000);
568
-
569
- // Set up the pipe in both directions
570
404
  socket.pipe(targetSocket);
571
405
  targetSocket.pipe(socket);
406
+
407
+ console.log(
408
+ `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
409
+ `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
410
+ );
572
411
 
573
- // Attach error and close handlers
412
+ // Add appropriate handlers for connection management
574
413
  socket.on('error', handleError('incoming'));
575
414
  targetSocket.on('error', handleError('outgoing'));
576
415
  socket.on('close', handleClose('incoming'));
577
416
  targetSocket.on('close', handleClose('outgoing'));
578
-
579
- // Handle timeout events
580
417
  socket.on('timeout', () => {
581
418
  console.log(`Timeout on incoming side from ${remoteIP}`);
582
419
  if (incomingTerminationReason === null) {
583
420
  incomingTerminationReason = 'timeout';
584
421
  this.incrementTerminationStat('incoming', 'timeout');
585
422
  }
586
- initiateCleanupOnce('timeout');
423
+ initiateCleanupOnce('timeout_incoming');
587
424
  });
588
-
589
425
  targetSocket.on('timeout', () => {
590
426
  console.log(`Timeout on outgoing side from ${remoteIP}`);
591
427
  if (outgoingTerminationReason === null) {
592
428
  outgoingTerminationReason = 'timeout';
593
429
  this.incrementTerminationStat('outgoing', 'timeout');
594
430
  }
595
- initiateCleanupOnce('timeout');
431
+ initiateCleanupOnce('timeout_outgoing');
596
432
  });
597
-
598
- socket.on('end', handleClose('incoming'));
599
- targetSocket.on('end', handleClose('outgoing'));
600
433
 
601
- // Track activity for both sockets to reset inactivity timers
602
- socket.on('data', (data) => {
603
- this.updateActivity(connectionRecord);
434
+ // Set appropriate timeouts
435
+ socket.setTimeout(120000);
436
+ targetSocket.setTimeout(120000);
437
+
438
+ // Update activity for both sockets
439
+ socket.on('data', () => {
440
+ connectionRecord.lastActivity = Date.now();
604
441
  });
605
442
 
606
- targetSocket.on('data', (data) => {
607
- this.updateActivity(connectionRecord);
443
+ targetSocket.on('data', () => {
444
+ connectionRecord.lastActivity = Date.now();
608
445
  });
609
446
 
610
447
  // Initialize a cleanup timer for max connection lifetime
@@ -623,7 +460,6 @@ export class PortProxy {
623
460
  if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
624
461
  console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
625
462
  socket.end();
626
- initiateCleanupOnce('rejected');
627
463
  return;
628
464
  }
629
465
  console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
@@ -652,7 +488,6 @@ export class PortProxy {
652
488
  if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
653
489
  console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
654
490
  socket.end();
655
- initiateCleanupOnce('rejected');
656
491
  return;
657
492
  }
658
493
  console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
@@ -740,7 +575,7 @@ export class PortProxy {
740
575
  this.netServers.push(server);
741
576
  }
742
577
 
743
- // Log active connection count, run parity checks, and check for connection issues every 10 seconds.
578
+ // Log active connection count, longest running durations, and run parity checks every 10 seconds.
744
579
  this.connectionLogger = setInterval(() => {
745
580
  if (this.isShuttingDown) return;
746
581
 
@@ -760,25 +595,23 @@ export class PortProxy {
760
595
  maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
761
596
  }
762
597
 
763
- // Parity check: if outgoing socket closed and incoming remains active for >30 seconds, trigger cleanup
598
+ // Parity check: if outgoing socket closed and incoming remains active
764
599
  if (record.outgoingClosedTime &&
765
600
  !record.incoming.destroyed &&
766
601
  !record.connectionClosed &&
767
- !record.cleanupInitiated &&
768
602
  (now - record.outgoingClosedTime > 30000)) {
769
603
  const remoteIP = record.incoming.remoteAddress || 'unknown';
770
- console.log(`Parity check triggered: Incoming socket for ${remoteIP} has been active >30s after outgoing closed.`);
771
- this.initiateCleanup(record, 'parity_check');
604
+ console.log(`Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`);
605
+ this.cleanupConnection(record, 'parity_check');
772
606
  }
773
607
 
774
- // Inactivity check: if no activity for a long time but sockets still open
608
+ // Inactivity check
775
609
  const inactivityTime = now - record.lastActivity;
776
610
  if (inactivityTime > 180000 && // 3 minutes
777
- !record.connectionClosed &&
778
- !record.cleanupInitiated) {
611
+ !record.connectionClosed) {
779
612
  const remoteIP = record.incoming.remoteAddress || 'unknown';
780
- console.log(`Inactivity check triggered: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
781
- this.initiateCleanup(record, 'inactivity');
613
+ console.log(`Inactivity check: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
614
+ this.cleanupConnection(record, 'inactivity');
782
615
  }
783
616
  }
784
617
 
@@ -813,14 +646,14 @@ export class PortProxy {
813
646
  await Promise.all(closeServerPromises);
814
647
  console.log("All servers closed. Cleaning up active connections...");
815
648
 
816
- // Gracefully close active connections
649
+ // Clean up active connections
817
650
  const connectionIds = [...this.connectionRecords.keys()];
818
651
  console.log(`Cleaning up ${connectionIds.length} active connections...`);
819
652
 
820
653
  for (const id of connectionIds) {
821
654
  const record = this.connectionRecords.get(id);
822
- if (record && !record.connectionClosed && !record.cleanupInitiated) {
823
- this.initiateCleanup(record, 'shutdown');
655
+ if (record && !record.connectionClosed) {
656
+ this.cleanupConnection(record, 'shutdown');
824
657
  }
825
658
  }
826
659