@push.rocks/smartproxy 19.5.7 → 19.5.10

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.
@@ -1,27 +1,62 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
 
3
+ export interface CleanupOptions {
4
+ immediate?: boolean; // Force immediate destruction
5
+ allowDrain?: boolean; // Allow write buffer to drain
6
+ gracePeriod?: number; // Ms to wait before force close
7
+ }
8
+
3
9
  /**
4
10
  * Safely cleanup a socket by removing all listeners and destroying it
5
11
  * @param socket The socket to cleanup
6
12
  * @param socketName Optional name for logging
13
+ * @param options Cleanup options
7
14
  */
8
- export function cleanupSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket | null, socketName?: string): void {
9
- if (!socket) return;
15
+ export function cleanupSocket(
16
+ socket: plugins.net.Socket | plugins.tls.TLSSocket | null,
17
+ socketName?: string,
18
+ options: CleanupOptions = {}
19
+ ): Promise<void> {
20
+ if (!socket || socket.destroyed) return Promise.resolve();
10
21
 
11
- try {
12
- // Remove all event listeners
13
- socket.removeAllListeners();
22
+ return new Promise<void>((resolve) => {
23
+ const cleanup = () => {
24
+ try {
25
+ // Remove all event listeners
26
+ socket.removeAllListeners();
27
+
28
+ // Destroy if not already destroyed
29
+ if (!socket.destroyed) {
30
+ socket.destroy();
31
+ }
32
+ } catch (err) {
33
+ console.error(`Error cleaning up socket${socketName ? ` (${socketName})` : ''}: ${err}`);
34
+ }
35
+ resolve();
36
+ };
14
37
 
15
- // Unpipe any streams
16
- socket.unpipe();
17
-
18
- // Destroy if not already destroyed
19
- if (!socket.destroyed) {
20
- socket.destroy();
38
+ if (options.immediate) {
39
+ // Immediate cleanup (old behavior)
40
+ socket.unpipe();
41
+ cleanup();
42
+ } else if (options.allowDrain && socket.writable) {
43
+ // Allow pending writes to complete
44
+ socket.end(() => cleanup());
45
+
46
+ // Force cleanup after grace period
47
+ if (options.gracePeriod) {
48
+ setTimeout(() => {
49
+ if (!socket.destroyed) {
50
+ cleanup();
51
+ }
52
+ }, options.gracePeriod);
53
+ }
54
+ } else {
55
+ // Default: immediate cleanup
56
+ socket.unpipe();
57
+ cleanup();
21
58
  }
22
- } catch (err) {
23
- console.error(`Error cleaning up socket${socketName ? ` (${socketName})` : ''}: ${err}`);
24
- }
59
+ });
25
60
  }
26
61
 
27
62
  /**
@@ -30,6 +65,7 @@ export function cleanupSocket(socket: plugins.net.Socket | plugins.tls.TLSSocket
30
65
  * @param serverSocket The server socket (optional)
31
66
  * @param onCleanup Optional callback when cleanup is done
32
67
  * @returns A cleanup function that can be called multiple times safely
68
+ * @deprecated Use createIndependentSocketHandlers for better half-open support
33
69
  */
34
70
  export function createSocketCleanupHandler(
35
71
  clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
@@ -42,10 +78,10 @@ export function createSocketCleanupHandler(
42
78
  if (cleanedUp) return;
43
79
  cleanedUp = true;
44
80
 
45
- // Cleanup both sockets
46
- cleanupSocket(clientSocket, 'client');
81
+ // Cleanup both sockets (old behavior - too aggressive)
82
+ cleanupSocket(clientSocket, 'client', { immediate: true });
47
83
  if (serverSocket) {
48
- cleanupSocket(serverSocket, 'server');
84
+ cleanupSocket(serverSocket, 'server', { immediate: true });
49
85
  }
50
86
 
51
87
  // Call cleanup callback if provided
@@ -55,15 +91,79 @@ export function createSocketCleanupHandler(
55
91
  };
56
92
  }
57
93
 
94
+ /**
95
+ * Create independent cleanup handlers for paired sockets that support half-open connections
96
+ * @param clientSocket The client socket
97
+ * @param serverSocket The server socket
98
+ * @param onBothClosed Callback when both sockets are closed
99
+ * @returns Independent cleanup functions for each socket
100
+ */
101
+ export function createIndependentSocketHandlers(
102
+ clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
103
+ serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
104
+ onBothClosed: (reason: string) => void
105
+ ): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
106
+ let clientClosed = false;
107
+ let serverClosed = false;
108
+ let clientReason = '';
109
+ let serverReason = '';
110
+
111
+ const checkBothClosed = () => {
112
+ if (clientClosed && serverClosed) {
113
+ onBothClosed(`client: ${clientReason}, server: ${serverReason}`);
114
+ }
115
+ };
116
+
117
+ const cleanupClient = async (reason: string) => {
118
+ if (clientClosed) return;
119
+ clientClosed = true;
120
+ clientReason = reason;
121
+
122
+ // Allow server to continue if still active
123
+ if (!serverClosed && serverSocket.writable) {
124
+ // Half-close: stop reading from client, let server finish
125
+ clientSocket.pause();
126
+ clientSocket.unpipe(serverSocket);
127
+ await cleanupSocket(clientSocket, 'client', { allowDrain: true, gracePeriod: 5000 });
128
+ } else {
129
+ await cleanupSocket(clientSocket, 'client', { immediate: true });
130
+ }
131
+
132
+ checkBothClosed();
133
+ };
134
+
135
+ const cleanupServer = async (reason: string) => {
136
+ if (serverClosed) return;
137
+ serverClosed = true;
138
+ serverReason = reason;
139
+
140
+ // Allow client to continue if still active
141
+ if (!clientClosed && clientSocket.writable) {
142
+ // Half-close: stop reading from server, let client finish
143
+ serverSocket.pause();
144
+ serverSocket.unpipe(clientSocket);
145
+ await cleanupSocket(serverSocket, 'server', { allowDrain: true, gracePeriod: 5000 });
146
+ } else {
147
+ await cleanupSocket(serverSocket, 'server', { immediate: true });
148
+ }
149
+
150
+ checkBothClosed();
151
+ };
152
+
153
+ return { cleanupClient, cleanupServer };
154
+ }
155
+
58
156
  /**
59
157
  * Setup socket error and close handlers with proper cleanup
60
158
  * @param socket The socket to setup handlers for
61
159
  * @param handleClose The cleanup function to call
160
+ * @param handleTimeout Optional custom timeout handler
62
161
  * @param errorPrefix Optional prefix for error messages
63
162
  */
64
163
  export function setupSocketHandlers(
65
164
  socket: plugins.net.Socket | plugins.tls.TLSSocket,
66
165
  handleClose: (reason: string) => void,
166
+ handleTimeout?: (socket: plugins.net.Socket | plugins.tls.TLSSocket) => void,
67
167
  errorPrefix?: string
68
168
  ): void {
69
169
  socket.on('error', (error) => {
@@ -77,8 +177,12 @@ export function setupSocketHandlers(
77
177
  });
78
178
 
79
179
  socket.on('timeout', () => {
80
- const prefix = errorPrefix || 'socket';
81
- handleClose(`${prefix}_timeout`);
180
+ if (handleTimeout) {
181
+ handleTimeout(socket); // Custom timeout handling
182
+ } else {
183
+ // Default: just log, don't close
184
+ console.warn(`Socket timeout: ${errorPrefix || 'socket'}`);
185
+ }
82
186
  });
83
187
  }
84
188
 
@@ -49,7 +49,12 @@ export class HttpForwardingHandler extends ForwardingHandler {
49
49
  });
50
50
  };
51
51
 
52
- setupSocketHandlers(socket, handleClose, 'http');
52
+ // Use custom timeout handler that doesn't close the socket
53
+ setupSocketHandlers(socket, handleClose, () => {
54
+ // For HTTP, we can be more aggressive with timeouts since connections are shorter
55
+ // But still don't close immediately - let the connection finish naturally
56
+ console.warn(`HTTP socket timeout from ${remoteAddress}`);
57
+ }, 'http');
53
58
 
54
59
  socket.on('error', (error) => {
55
60
  this.emit(ForwardingHandlerEvents.ERROR, {
@@ -2,7 +2,7 @@ import * as plugins from '../../plugins.js';
2
2
  import { ForwardingHandler } from './base-handler.js';
3
3
  import type { IForwardConfig } from '../config/forwarding-types.js';
4
4
  import { ForwardingHandlerEvents } from '../config/forwarding-types.js';
5
- import { createSocketCleanupHandler, setupSocketHandlers, pipeSockets } from '../../core/utils/socket-utils.js';
5
+ import { createIndependentSocketHandlers, setupSocketHandlers } from '../../core/utils/socket-utils.js';
6
6
 
7
7
  /**
8
8
  * Handler for HTTPS passthrough (SNI forwarding without termination)
@@ -55,19 +55,32 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
55
55
  let bytesSent = 0;
56
56
  let bytesReceived = 0;
57
57
 
58
- // Create cleanup handler with our utility
59
- const handleClose = createSocketCleanupHandler(clientSocket, serverSocket, (reason) => {
60
- this.emit(ForwardingHandlerEvents.DISCONNECTED, {
61
- remoteAddress,
62
- bytesSent,
63
- bytesReceived,
64
- reason
65
- });
66
- });
58
+ // Create independent handlers for half-open connection support
59
+ const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
60
+ clientSocket,
61
+ serverSocket,
62
+ (reason) => {
63
+ this.emit(ForwardingHandlerEvents.DISCONNECTED, {
64
+ remoteAddress,
65
+ bytesSent,
66
+ bytesReceived,
67
+ reason
68
+ });
69
+ }
70
+ );
67
71
 
68
- // Setup error and close handlers for both sockets
69
- setupSocketHandlers(serverSocket, handleClose, 'server');
70
- setupSocketHandlers(clientSocket, handleClose, 'client');
72
+ // Setup handlers with custom timeout handling that doesn't close connections
73
+ const timeout = this.getTimeout();
74
+
75
+ setupSocketHandlers(clientSocket, cleanupClient, (socket) => {
76
+ // Just reset timeout, don't close
77
+ socket.setTimeout(timeout);
78
+ }, 'client');
79
+
80
+ setupSocketHandlers(serverSocket, cleanupServer, (socket) => {
81
+ // Just reset timeout, don't close
82
+ socket.setTimeout(timeout);
83
+ }, 'server');
71
84
 
72
85
  // Forward data from client to server
73
86
  clientSocket.on('data', (data) => {
@@ -117,8 +130,7 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
117
130
  });
118
131
  });
119
132
 
120
- // Set timeouts
121
- const timeout = this.getTimeout();
133
+ // Set initial timeouts - they will be reset on each timeout event
122
134
  clientSocket.setTimeout(timeout);
123
135
  serverSocket.setTimeout(timeout);
124
136
  }
@@ -128,7 +140,7 @@ export class HttpsPassthroughHandler extends ForwardingHandler {
128
140
  * @param req The HTTP request
129
141
  * @param res The HTTP response
130
142
  */
131
- public handleHttpRequest(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
143
+ public handleHttpRequest(_req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse): void {
132
144
  // HTTPS passthrough doesn't support HTTP requests
133
145
  res.writeHead(404, { 'Content-Type': 'text/plain' });
134
146
  res.end('HTTP not supported for this domain');
@@ -112,7 +112,7 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
112
112
  });
113
113
 
114
114
  // Set up error handling with our cleanup utility
115
- setupSocketHandlers(tlsSocket, handleClose, 'tls');
115
+ setupSocketHandlers(tlsSocket, handleClose, undefined, 'tls');
116
116
 
117
117
  // Set timeout
118
118
  const timeout = this.getTimeout();
@@ -167,7 +167,7 @@ export class HttpsTerminateToHttpHandler extends ForwardingHandler {
167
167
  });
168
168
 
169
169
  // Set up handlers for backend socket
170
- setupSocketHandlers(backendSocket, newHandleClose, 'backend');
170
+ setupSocketHandlers(backendSocket, newHandleClose, undefined, 'backend');
171
171
 
172
172
  backendSocket.on('error', (error) => {
173
173
  this.emit(ForwardingHandlerEvents.ERROR, {
@@ -106,7 +106,7 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
106
106
  });
107
107
 
108
108
  // Set up error handling with our cleanup utility
109
- setupSocketHandlers(tlsSocket, handleClose, 'tls');
109
+ setupSocketHandlers(tlsSocket, handleClose, undefined, 'tls');
110
110
 
111
111
  // Set timeout
112
112
  const timeout = this.getTimeout();
@@ -151,7 +151,7 @@ export class HttpsTerminateToHttpsHandler extends ForwardingHandler {
151
151
  });
152
152
 
153
153
  // Set up handlers for backend socket
154
- setupSocketHandlers(backendSocket, newHandleClose, 'backend');
154
+ setupSocketHandlers(backendSocket, newHandleClose, undefined, 'backend');
155
155
 
156
156
  backendSocket.on('error', (error) => {
157
157
  this.emit(ForwardingHandlerEvents.ERROR, {
@@ -134,7 +134,7 @@ export class ConnectionPool {
134
134
  if ((connection.isIdle && now - connection.lastUsed > idleTimeout) ||
135
135
  connections.length > (this.options.connectionPoolSize || 50)) {
136
136
 
137
- cleanupSocket(connection.socket, `pool-${host}-idle`);
137
+ cleanupSocket(connection.socket, `pool-${host}-idle`, { immediate: true }).catch(() => {});
138
138
 
139
139
  connections.shift(); // Remove from pool
140
140
  removed++;
@@ -164,7 +164,7 @@ export class ConnectionPool {
164
164
  this.logger.debug(`Closing ${connections.length} connections to ${host}`);
165
165
 
166
166
  for (const connection of connections) {
167
- cleanupSocket(connection.socket, `pool-${host}-close`);
167
+ cleanupSocket(connection.socket, `pool-${host}-close`, { immediate: true }).catch(() => {});
168
168
  }
169
169
  }
170
170
 
@@ -520,9 +520,10 @@ export class HttpProxy implements IMetricsTracker {
520
520
  this.webSocketHandler.shutdown();
521
521
 
522
522
  // Close all tracked sockets
523
- for (const socket of this.socketMap.getArray()) {
524
- cleanupSocket(socket, 'http-proxy-stop');
525
- }
523
+ const socketCleanupPromises = this.socketMap.getArray().map(socket =>
524
+ cleanupSocket(socket, 'http-proxy-stop', { immediate: true })
525
+ );
526
+ await Promise.all(socketCleanupPromises);
526
527
 
527
528
  // Close all connection pool connections
528
529
  this.connectionPool.closeAllConnections();
@@ -278,12 +278,37 @@ export class ConnectionManager extends LifecycleComponent {
278
278
  }
279
279
  }
280
280
 
281
- // Handle socket cleanup without delay
282
- cleanupSocket(record.incoming, `${record.id}-incoming`);
281
+ // Handle socket cleanup - check if sockets are still active
282
+ const cleanupPromises: Promise<void>[] = [];
283
+
284
+ if (record.incoming) {
285
+ if (!record.incoming.writable || record.incoming.destroyed) {
286
+ // Socket is not active, clean up immediately
287
+ cleanupPromises.push(cleanupSocket(record.incoming, `${record.id}-incoming`, { immediate: true }));
288
+ } else {
289
+ // Socket is still active, allow graceful cleanup
290
+ cleanupPromises.push(cleanupSocket(record.incoming, `${record.id}-incoming`, { allowDrain: true, gracePeriod: 5000 }));
291
+ }
292
+ }
283
293
 
284
294
  if (record.outgoing) {
285
- cleanupSocket(record.outgoing, `${record.id}-outgoing`);
295
+ if (!record.outgoing.writable || record.outgoing.destroyed) {
296
+ // Socket is not active, clean up immediately
297
+ cleanupPromises.push(cleanupSocket(record.outgoing, `${record.id}-outgoing`, { immediate: true }));
298
+ } else {
299
+ // Socket is still active, allow graceful cleanup
300
+ cleanupPromises.push(cleanupSocket(record.outgoing, `${record.id}-outgoing`, { allowDrain: true, gracePeriod: 5000 }));
301
+ }
286
302
  }
303
+
304
+ // Wait for cleanup to complete
305
+ Promise.all(cleanupPromises).catch(err => {
306
+ logger.log('error', `Error during socket cleanup: ${err}`, {
307
+ connectionId: record.id,
308
+ error: err,
309
+ component: 'connection-manager'
310
+ });
311
+ });
287
312
 
288
313
  // Clear pendingData to avoid memory leaks
289
314
  record.pendingData = [];
@@ -484,19 +509,24 @@ export class ConnectionManager extends LifecycleComponent {
484
509
  }
485
510
 
486
511
  // Parity check: if outgoing socket closed and incoming remains active
512
+ // Increased from 2 minutes to 30 minutes for long-lived connections
487
513
  if (
488
514
  record.outgoingClosedTime &&
489
515
  !record.incoming.destroyed &&
490
516
  !record.connectionClosed &&
491
- now - record.outgoingClosedTime > 120000
517
+ now - record.outgoingClosedTime > 1800000 // 30 minutes
492
518
  ) {
493
- logger.log('warn', `Parity check failed: ${record.remoteIP}`, {
494
- connectionId,
495
- remoteIP: record.remoteIP,
496
- timeElapsed: plugins.prettyMs(now - record.outgoingClosedTime),
497
- component: 'connection-manager'
498
- });
499
- this.cleanupConnection(record, 'parity_check');
519
+ // Only close if no data activity for 10 minutes
520
+ if (now - record.lastActivity > 600000) {
521
+ logger.log('warn', `Parity check failed after extended timeout: ${record.remoteIP}`, {
522
+ connectionId,
523
+ remoteIP: record.remoteIP,
524
+ timeElapsed: plugins.prettyMs(now - record.outgoingClosedTime),
525
+ inactiveFor: plugins.prettyMs(now - record.lastActivity),
526
+ component: 'connection-manager'
527
+ });
528
+ this.cleanupConnection(record, 'parity_check');
529
+ }
500
530
  }
501
531
  }
502
532
  }
@@ -537,13 +567,18 @@ export class ConnectionManager extends LifecycleComponent {
537
567
  }
538
568
 
539
569
  // Immediate destruction using socket-utils
570
+ const shutdownPromises: Promise<void>[] = [];
571
+
540
572
  if (record.incoming) {
541
- cleanupSocket(record.incoming, `${record.id}-incoming-shutdown`);
573
+ shutdownPromises.push(cleanupSocket(record.incoming, `${record.id}-incoming-shutdown`, { immediate: true }));
542
574
  }
543
575
 
544
576
  if (record.outgoing) {
545
- cleanupSocket(record.outgoing, `${record.id}-outgoing-shutdown`);
577
+ shutdownPromises.push(cleanupSocket(record.outgoing, `${record.id}-outgoing-shutdown`, { immediate: true }));
546
578
  }
579
+
580
+ // Don't wait for shutdown cleanup in this batch processing
581
+ Promise.all(shutdownPromises).catch(() => {});
547
582
  } catch (err) {
548
583
  logger.log('error', `Error during connection cleanup: ${err}`, {
549
584
  connectionId: record.id,
@@ -65,7 +65,7 @@ export class PortManager {
65
65
  const server = plugins.net.createServer((socket) => {
66
66
  // Check if shutting down
67
67
  if (this.isShuttingDown) {
68
- cleanupSocket(socket, 'port-manager-shutdown');
68
+ cleanupSocket(socket, 'port-manager-shutdown', { immediate: true });
69
69
  return;
70
70
  }
71
71
 
@@ -9,7 +9,7 @@ import { TlsManager } from './tls-manager.js';
9
9
  import { HttpProxyBridge } from './http-proxy-bridge.js';
10
10
  import { TimeoutManager } from './timeout-manager.js';
11
11
  import { RouteManager } from './route-manager.js';
12
- import { cleanupSocket } from '../../core/utils/socket-utils.js';
12
+ import { cleanupSocket, createIndependentSocketHandlers, setupSocketHandlers } from '../../core/utils/socket-utils.js';
13
13
 
14
14
  /**
15
15
  * Handles new connection processing and setup logic with support for route-based configuration
@@ -84,7 +84,7 @@ export class RouteConnectionHandler {
84
84
  const ipValidation = this.securityManager.validateIP(remoteIP);
85
85
  if (!ipValidation.allowed) {
86
86
  logger.log('warn', `Connection rejected`, { remoteIP, reason: ipValidation.reason, component: 'route-handler' });
87
- cleanupSocket(socket, `rejected-${ipValidation.reason}`);
87
+ cleanupSocket(socket, `rejected-${ipValidation.reason}`, { immediate: true });
88
88
  return;
89
89
  }
90
90
 
@@ -1110,9 +1110,8 @@ export class RouteConnectionHandler {
1110
1110
  // Setup improved error handling for outgoing connection
1111
1111
  this.setupOutgoingErrorHandler(connectionId, targetSocket, record, socket, finalTargetHost, finalTargetPort);
1112
1112
 
1113
- // Setup close handlers
1114
- targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
1115
- socket.on('close', this.connectionManager.handleClose('incoming', record));
1113
+ // Note: Close handlers are managed by independent socket handlers above
1114
+ // We don't register handleClose here to avoid bilateral cleanup
1116
1115
 
1117
1116
  // Setup error handlers for incoming socket
1118
1117
  socket.on('error', this.connectionManager.handleError('incoming', record));
@@ -1225,14 +1224,64 @@ export class RouteConnectionHandler {
1225
1224
  record.pendingDataSize = 0;
1226
1225
  }
1227
1226
 
1228
- // Immediately setup bidirectional piping - much simpler than manual data management
1229
- socket.pipe(targetSocket);
1230
- targetSocket.pipe(socket);
1227
+ // Set up independent socket handlers for half-open connection support
1228
+ const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
1229
+ socket,
1230
+ targetSocket,
1231
+ (reason) => {
1232
+ this.connectionManager.initiateCleanupOnce(record, reason);
1233
+ }
1234
+ );
1231
1235
 
1232
- // Track incoming data for bytes counting - do this after piping is set up
1236
+ // Setup socket handlers with custom timeout handling
1237
+ setupSocketHandlers(socket, cleanupClient, (sock) => {
1238
+ // Don't close on timeout for keep-alive connections
1239
+ if (record.hasKeepAlive) {
1240
+ sock.setTimeout(this.settings.socketTimeout || 3600000);
1241
+ }
1242
+ }, 'client');
1243
+
1244
+ setupSocketHandlers(targetSocket, cleanupServer, (sock) => {
1245
+ // Don't close on timeout for keep-alive connections
1246
+ if (record.hasKeepAlive) {
1247
+ sock.setTimeout(this.settings.socketTimeout || 3600000);
1248
+ }
1249
+ }, 'server');
1250
+
1251
+ // Forward data from client to target with backpressure handling
1233
1252
  socket.on('data', (chunk: Buffer) => {
1234
1253
  record.bytesReceived += chunk.length;
1235
1254
  this.timeoutManager.updateActivity(record);
1255
+
1256
+ if (targetSocket.writable) {
1257
+ const flushed = targetSocket.write(chunk);
1258
+
1259
+ // Handle backpressure
1260
+ if (!flushed) {
1261
+ socket.pause();
1262
+ targetSocket.once('drain', () => {
1263
+ socket.resume();
1264
+ });
1265
+ }
1266
+ }
1267
+ });
1268
+
1269
+ // Forward data from target to client with backpressure handling
1270
+ targetSocket.on('data', (chunk: Buffer) => {
1271
+ record.bytesSent += chunk.length;
1272
+ this.timeoutManager.updateActivity(record);
1273
+
1274
+ if (socket.writable) {
1275
+ const flushed = socket.write(chunk);
1276
+
1277
+ // Handle backpressure
1278
+ if (!flushed) {
1279
+ targetSocket.pause();
1280
+ socket.once('drain', () => {
1281
+ targetSocket.resume();
1282
+ });
1283
+ }
1284
+ }
1236
1285
  });
1237
1286
 
1238
1287
  // Log successful connection