@push.rocks/smartproxy 3.22.0 → 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.
@@ -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,55 +286,25 @@ 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
- // Set an initial timeout only if SNI is enabled or this is not a chained proxy
328
- // For chained proxies, we need to allow more time for data to flow through
329
- const initialTimeoutMs = this.settings.initialDataTimeout ||
330
- (this.settings.sniEnabled ? 15000 : 0); // Increased timeout for SNI, disabled for non-SNI by default
331
-
292
+ // Set an initial timeout for SNI data if needed
332
293
  let initialTimeout: NodeJS.Timeout | null = null;
333
-
334
- if (initialTimeoutMs > 0) {
335
- console.log(`Setting initial data timeout of ${initialTimeoutMs}ms for connection from ${remoteIP}`);
294
+ if (this.settings.sniEnabled) {
336
295
  initialTimeout = setTimeout(() => {
337
296
  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');
297
+ console.log(`Initial data timeout for ${remoteIP}`);
298
+ socket.end();
299
+ cleanupOnce();
344
300
  }
345
- }, initialTimeoutMs);
301
+ }, 5000);
346
302
  } else {
347
- console.log(`No initial timeout set for connection from ${remoteIP} (likely chained proxy)`);
348
- // Mark as received immediately if we're not waiting for data
349
303
  initialDataReceived = true;
350
304
  }
351
305
 
352
306
  socket.on('error', (err: Error) => {
353
- const errorMessage = initialDataReceived
354
- ? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
355
- : `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
356
- console.log(errorMessage);
357
-
358
- // Clear the initial timeout if it exists
359
- if (initialTimeout) {
360
- clearTimeout(initialTimeout);
361
- initialTimeout = null;
362
- }
363
-
364
- // For premature errors, we need to handle them explicitly
365
- // since the standard error handlers might not be set up yet
366
- if (!initialDataReceived) {
367
- if (incomingTerminationReason === null) {
368
- incomingTerminationReason = 'premature_error';
369
- this.incrementTerminationStat('incoming', 'premature_error');
370
- }
371
- initiateCleanupOnce('premature_error');
372
- }
307
+ console.log(`Incoming socket error from ${remoteIP}: ${err.message}`);
373
308
  });
374
309
 
375
310
  const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
@@ -378,13 +313,9 @@ export class PortProxy {
378
313
  if (code === 'ECONNRESET') {
379
314
  reason = 'econnreset';
380
315
  console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
381
- } else if (code === 'ECONNREFUSED') {
382
- reason = 'econnrefused';
383
- console.log(`ECONNREFUSED on ${side} side from ${remoteIP}: ${err.message}`);
384
316
  } else {
385
317
  console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
386
318
  }
387
-
388
319
  if (side === 'incoming' && incomingTerminationReason === null) {
389
320
  incomingTerminationReason = reason;
390
321
  this.incrementTerminationStat('incoming', reason);
@@ -392,13 +323,11 @@ export class PortProxy {
392
323
  outgoingTerminationReason = reason;
393
324
  this.incrementTerminationStat('outgoing', reason);
394
325
  }
395
-
396
326
  initiateCleanupOnce(reason);
397
327
  };
398
328
 
399
329
  const handleClose = (side: 'incoming' | 'outgoing') => () => {
400
330
  console.log(`Connection closed on ${side} side from ${remoteIP}`);
401
-
402
331
  if (side === 'incoming' && incomingTerminationReason === null) {
403
332
  incomingTerminationReason = 'normal';
404
333
  this.incrementTerminationStat('incoming', 'normal');
@@ -407,24 +336,8 @@ export class PortProxy {
407
336
  this.incrementTerminationStat('outgoing', 'normal');
408
337
  // Record the time when outgoing socket closed.
409
338
  connectionRecord.outgoingClosedTime = Date.now();
410
-
411
- // If incoming is still active but outgoing closed, set a shorter timeout
412
- if (!connectionRecord.incoming.destroyed) {
413
- console.log(`Outgoing socket closed but incoming still active for ${remoteIP}. Setting cleanup timeout.`);
414
- setTimeout(() => {
415
- if (!connectionRecord.connectionClosed && !connectionRecord.incoming.destroyed) {
416
- console.log(`Incoming socket still active ${Date.now() - connectionRecord.outgoingClosedTime!}ms after outgoing closed for ${remoteIP}. Cleaning up.`);
417
- initiateCleanupOnce('outgoing_closed_timeout');
418
- }
419
- }, 10000); // 10 second timeout instead of waiting for the next parity check
420
- }
421
- }
422
-
423
- // If both sides are closed/destroyed, clean up
424
- if ((side === 'incoming' && connectionRecord.outgoing?.destroyed) ||
425
- (side === 'outgoing' && connectionRecord.incoming.destroyed)) {
426
- initiateCleanupOnce('both_closed');
427
339
  }
340
+ initiateCleanupOnce('closed_' + side);
428
341
  };
429
342
 
430
343
  /**
@@ -438,6 +351,7 @@ export class PortProxy {
438
351
  // Clear the initial timeout since we've received data
439
352
  if (initialTimeout) {
440
353
  clearTimeout(initialTimeout);
354
+ initialTimeout = null;
441
355
  }
442
356
 
443
357
  // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
@@ -447,35 +361,25 @@ export class PortProxy {
447
361
  config.domains.some(d => plugins.minimatch(serverName, d))
448
362
  ) : undefined);
449
363
 
450
- // Effective IP check: merge allowed IPs with default allowed, and remove blocked IPs.
451
- // In a chained proxy, relax IP validation unless explicitly configured
452
- // If this is the first proxy in the chain, normal validation applies
364
+ // IP validation is skipped if allowedIPs is empty
453
365
  if (domainConfig) {
454
- // Has specific domain config - check IP restrictions only if allowedIPs is non-empty
455
- if (domainConfig.allowedIPs.length > 0) {
456
- const effectiveAllowedIPs: string[] = [
457
- ...domainConfig.allowedIPs,
458
- ...(this.settings.defaultAllowedIPs || [])
459
- ];
460
- const effectiveBlockedIPs: string[] = [
461
- ...(domainConfig.blockedIPs || []),
462
- ...(this.settings.defaultBlockedIPs || [])
463
- ];
464
- if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
465
- return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
466
- }
467
- } else {
468
- console.log(`Domain config for ${domainConfig.domains.join(', ')} has empty allowedIPs, skipping IP validation`);
366
+ const effectiveAllowedIPs: string[] = [
367
+ ...domainConfig.allowedIPs,
368
+ ...(this.settings.defaultAllowedIPs || [])
369
+ ];
370
+ const effectiveBlockedIPs: string[] = [
371
+ ...(domainConfig.blockedIPs || []),
372
+ ...(this.settings.defaultBlockedIPs || [])
373
+ ];
374
+
375
+ // Skip IP validation if allowedIPs is empty
376
+ if (domainConfig.allowedIPs.length > 0 && !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
377
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
469
378
  }
470
379
  } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
471
- // No domain config but has default IP restrictions
472
380
  if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
473
381
  return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
474
382
  }
475
- } else {
476
- // No domain config and no default allowed IPs
477
- // In a chained proxy setup, we'll allow this connection
478
- console.log(`No specific IP restrictions found for ${remoteIP}. Allowing connection in potential chained proxy setup.`);
479
383
  }
480
384
 
481
385
  const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
@@ -487,116 +391,57 @@ export class PortProxy {
487
391
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
488
392
  }
489
393
 
490
- // Add explicit connection timeout and error handling
491
- let connectionTimeout: NodeJS.Timeout | null = null;
492
- let connectionSucceeded = false;
493
-
494
- // Set connection timeout - longer for chained proxies
495
- connectionTimeout = setTimeout(() => {
496
- if (!connectionSucceeded) {
497
- console.log(`Connection timeout connecting to ${targetHost}:${connectionOptions.port} for ${remoteIP}`);
498
- if (outgoingTerminationReason === null) {
499
- outgoingTerminationReason = 'connection_timeout';
500
- this.incrementTerminationStat('outgoing', 'connection_timeout');
501
- }
502
- initiateCleanupOnce('connection_timeout');
503
- }
504
- }, 10000); // Increased from 5s to 10s to accommodate chained proxies
505
-
506
- console.log(`Attempting to connect to ${targetHost}:${connectionOptions.port} for client ${remoteIP}...`);
507
-
508
- // Create the target socket
394
+ // Create the target socket and immediately set up data piping
509
395
  const targetSocket = plugins.net.connect(connectionOptions);
510
396
  connectionRecord.outgoing = targetSocket;
397
+ connectionRecord.outgoingStartTime = Date.now();
511
398
 
512
- // Handle successful connection
513
- targetSocket.once('connect', () => {
514
- connectionSucceeded = true;
515
- if (connectionTimeout) {
516
- clearTimeout(connectionTimeout);
517
- connectionTimeout = null;
518
- }
519
-
520
- connectionRecord.outgoingStartTime = Date.now();
521
- console.log(
522
- `Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
523
- `${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
524
- );
525
-
526
- // Setup data flow after confirmed connection
527
- setupDataFlow(targetSocket, initialChunk);
528
- });
529
-
530
- // Handle connection errors early
531
- targetSocket.once('error', (err) => {
532
- if (!connectionSucceeded) {
533
- // This is an initial connection error
534
- console.log(`Failed to connect to ${targetHost}:${connectionOptions.port} for ${remoteIP}: ${err.message}`);
535
- if (connectionTimeout) {
536
- clearTimeout(connectionTimeout);
537
- connectionTimeout = null;
538
- }
539
- if (outgoingTerminationReason === null) {
540
- outgoingTerminationReason = 'connection_failed';
541
- this.incrementTerminationStat('outgoing', 'connection_failed');
542
- }
543
- initiateCleanupOnce('connection_failed');
544
- }
545
- // Other errors will be handled by the main error handler
546
- });
547
- };
548
-
549
- /**
550
- * Sets up the data flow between sockets after successful connection
551
- */
552
- const setupDataFlow = (targetSocket: plugins.net.Socket, initialChunk?: Buffer) => {
399
+ // Set up the pipe immediately to ensure data flows without delay
553
400
  if (initialChunk) {
554
401
  socket.unshift(initialChunk);
555
402
  }
556
403
 
557
- // Set appropriate timeouts for both sockets
558
- socket.setTimeout(120000);
559
- targetSocket.setTimeout(120000);
560
-
561
- // Set up the pipe in both directions
562
404
  socket.pipe(targetSocket);
563
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
+ );
564
411
 
565
- // Attach error and close handlers
412
+ // Add appropriate handlers for connection management
566
413
  socket.on('error', handleError('incoming'));
567
414
  targetSocket.on('error', handleError('outgoing'));
568
415
  socket.on('close', handleClose('incoming'));
569
416
  targetSocket.on('close', handleClose('outgoing'));
570
-
571
- // Handle timeout events
572
417
  socket.on('timeout', () => {
573
418
  console.log(`Timeout on incoming side from ${remoteIP}`);
574
419
  if (incomingTerminationReason === null) {
575
420
  incomingTerminationReason = 'timeout';
576
421
  this.incrementTerminationStat('incoming', 'timeout');
577
422
  }
578
- initiateCleanupOnce('timeout');
423
+ initiateCleanupOnce('timeout_incoming');
579
424
  });
580
-
581
425
  targetSocket.on('timeout', () => {
582
426
  console.log(`Timeout on outgoing side from ${remoteIP}`);
583
427
  if (outgoingTerminationReason === null) {
584
428
  outgoingTerminationReason = 'timeout';
585
429
  this.incrementTerminationStat('outgoing', 'timeout');
586
430
  }
587
- initiateCleanupOnce('timeout');
431
+ initiateCleanupOnce('timeout_outgoing');
588
432
  });
589
-
590
- socket.on('end', handleClose('incoming'));
591
- targetSocket.on('end', handleClose('outgoing'));
592
433
 
593
- // Track activity for both sockets to reset inactivity timers
594
- socket.on('data', (data) => {
595
- 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();
596
441
  });
597
442
 
598
- targetSocket.on('data', (data) => {
599
- this.updateActivity(connectionRecord);
443
+ targetSocket.on('data', () => {
444
+ connectionRecord.lastActivity = Date.now();
600
445
  });
601
446
 
602
447
  // Initialize a cleanup timer for max connection lifetime
@@ -615,7 +460,6 @@ export class PortProxy {
615
460
  if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
616
461
  console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
617
462
  socket.end();
618
- initiateCleanupOnce('rejected');
619
463
  return;
620
464
  }
621
465
  console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
@@ -644,7 +488,6 @@ export class PortProxy {
644
488
  if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
645
489
  console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
646
490
  socket.end();
647
- initiateCleanupOnce('rejected');
648
491
  return;
649
492
  }
650
493
  console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
@@ -657,14 +500,8 @@ export class PortProxy {
657
500
 
658
501
  // --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
659
502
  if (this.settings.sniEnabled) {
660
- // If using SNI, we need to wait for data to establish the connection
661
- if (initialDataReceived) {
662
- console.log(`Initial data already marked as received for ${remoteIP}, but SNI is enabled. This is unexpected.`);
663
- }
664
-
665
503
  initialDataReceived = false;
666
-
667
- console.log(`Waiting for TLS ClientHello from ${remoteIP} to extract SNI...`);
504
+
668
505
  socket.once('data', (chunk: Buffer) => {
669
506
  if (initialTimeout) {
670
507
  clearTimeout(initialTimeout);
@@ -672,23 +509,10 @@ export class PortProxy {
672
509
  }
673
510
 
674
511
  initialDataReceived = true;
675
- console.log(`Received initial data from ${remoteIP}, length: ${chunk.length} bytes`);
676
-
677
- let serverName = '';
678
- try {
679
- // Only try to extract SNI if the chunk looks like a TLS ClientHello
680
- if (chunk.length > 5 && chunk.readUInt8(0) === 22) {
681
- serverName = extractSNI(chunk) || '';
682
- console.log(`Extracted SNI: "${serverName}" from connection ${remoteIP}`);
683
- } else {
684
- console.log(`Data from ${remoteIP} doesn't appear to be a TLS ClientHello. First byte: ${chunk.length > 0 ? chunk.readUInt8(0) : 'N/A'}`);
685
- }
686
- } catch (err) {
687
- console.log(`Error extracting SNI from chunk: ${err}. Proceeding without SNI.`);
688
- }
689
-
512
+ const serverName = extractSNI(chunk) || '';
690
513
  // Lock the connection to the negotiated SNI.
691
514
  connectionRecord.lockedDomain = serverName;
515
+ console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
692
516
 
693
517
  // Delay adding the renegotiation listener until the next tick,
694
518
  // so the initial ClientHello is not reprocessed.
@@ -714,23 +538,10 @@ export class PortProxy {
714
538
  setupConnection(serverName, chunk);
715
539
  });
716
540
  } else {
717
- // Non-SNI mode: we can proceed immediately without waiting for data
718
- if (initialTimeout) {
719
- clearTimeout(initialTimeout);
720
- initialTimeout = null;
721
- }
722
-
723
541
  initialDataReceived = true;
724
- console.log(`SNI disabled for connection from ${remoteIP}, proceeding directly to connection setup`);
725
-
726
- // Check IP restrictions only if explicitly configured
727
- if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
728
- if (!isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
729
- return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
730
- }
542
+ if (!this.settings.defaultAllowedIPs || this.settings.defaultAllowedIPs.length === 0 || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
543
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
731
544
  }
732
-
733
- // Proceed with connection setup
734
545
  setupConnection('');
735
546
  }
736
547
  };
@@ -764,7 +575,7 @@ export class PortProxy {
764
575
  this.netServers.push(server);
765
576
  }
766
577
 
767
- // 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.
768
579
  this.connectionLogger = setInterval(() => {
769
580
  if (this.isShuttingDown) return;
770
581
 
@@ -784,25 +595,23 @@ export class PortProxy {
784
595
  maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
785
596
  }
786
597
 
787
- // 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
788
599
  if (record.outgoingClosedTime &&
789
600
  !record.incoming.destroyed &&
790
601
  !record.connectionClosed &&
791
- !record.cleanupInitiated &&
792
602
  (now - record.outgoingClosedTime > 30000)) {
793
603
  const remoteIP = record.incoming.remoteAddress || 'unknown';
794
- console.log(`Parity check triggered: Incoming socket for ${remoteIP} has been active >30s after outgoing closed.`);
795
- 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');
796
606
  }
797
607
 
798
- // Inactivity check: if no activity for a long time but sockets still open
608
+ // Inactivity check
799
609
  const inactivityTime = now - record.lastActivity;
800
610
  if (inactivityTime > 180000 && // 3 minutes
801
- !record.connectionClosed &&
802
- !record.cleanupInitiated) {
611
+ !record.connectionClosed) {
803
612
  const remoteIP = record.incoming.remoteAddress || 'unknown';
804
- console.log(`Inactivity check triggered: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
805
- this.initiateCleanup(record, 'inactivity');
613
+ console.log(`Inactivity check: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
614
+ this.cleanupConnection(record, 'inactivity');
806
615
  }
807
616
  }
808
617
 
@@ -837,14 +646,14 @@ export class PortProxy {
837
646
  await Promise.all(closeServerPromises);
838
647
  console.log("All servers closed. Cleaning up active connections...");
839
648
 
840
- // Gracefully close active connections
649
+ // Clean up active connections
841
650
  const connectionIds = [...this.connectionRecords.keys()];
842
651
  console.log(`Cleaning up ${connectionIds.length} active connections...`);
843
652
 
844
653
  for (const id of connectionIds) {
845
654
  const record = this.connectionRecords.get(id);
846
- if (record && !record.connectionClosed && !record.cleanupInitiated) {
847
- this.initiateCleanup(record, 'shutdown');
655
+ if (record && !record.connectionClosed) {
656
+ this.cleanupConnection(record, 'shutdown');
848
657
  }
849
658
  }
850
659