@push.rocks/smartproxy 3.10.1 → 3.10.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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '3.10.1',
6
+ version: '3.10.3',
7
7
  description: 'a proxy for handling high workloads of proxying'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLGlEQUFpRDtDQUMvRCxDQUFBIn0=
@@ -1,77 +1,54 @@
1
1
  import * as plugins from './plugins.js';
2
2
  /**
3
- * Extract SNI (Server Name Indication) from a TLS ClientHello packet.
4
- * Returns the server name if found, or undefined.
3
+ * Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
4
+ * @param buffer - Buffer containing the TLS ClientHello.
5
+ * @returns The server name if found, otherwise undefined.
5
6
  */
6
7
  function extractSNI(buffer) {
7
8
  let offset = 0;
8
- // We need at least 5 bytes for the record header.
9
- if (buffer.length < 5) {
9
+ if (buffer.length < 5)
10
10
  return undefined;
11
- }
12
- // TLS record header
13
11
  const recordType = buffer.readUInt8(0);
14
- if (recordType !== 22) { // 22 = handshake
15
- return undefined;
16
- }
17
- // Read record length
12
+ if (recordType !== 22)
13
+ return undefined; // 22 = handshake
18
14
  const recordLength = buffer.readUInt16BE(3);
19
- if (buffer.length < 5 + recordLength) {
20
- // Not all data arrived yet; in production you might need to accumulate more data.
15
+ if (buffer.length < 5 + recordLength)
21
16
  return undefined;
22
- }
23
17
  offset = 5;
24
- // Handshake message type should be 1 for ClientHello.
25
18
  const handshakeType = buffer.readUInt8(offset);
26
- if (handshakeType !== 1) {
27
- return undefined;
28
- }
29
- // Skip handshake header (1 byte type + 3 bytes length)
30
- offset += 4;
31
- // Skip client version (2 bytes) and random (32 bytes)
32
- offset += 2 + 32;
33
- // Session ID
19
+ if (handshakeType !== 1)
20
+ return undefined; // 1 = ClientHello
21
+ offset += 4; // Skip handshake header (type + length)
22
+ offset += 2 + 32; // Skip client version and random
34
23
  const sessionIDLength = buffer.readUInt8(offset);
35
- offset += 1 + sessionIDLength;
36
- // Cipher suites
24
+ offset += 1 + sessionIDLength; // Skip session ID
37
25
  const cipherSuitesLength = buffer.readUInt16BE(offset);
38
- offset += 2 + cipherSuitesLength;
39
- // Compression methods
26
+ offset += 2 + cipherSuitesLength; // Skip cipher suites
40
27
  const compressionMethodsLength = buffer.readUInt8(offset);
41
- offset += 1 + compressionMethodsLength;
42
- // Extensions length
43
- if (offset + 2 > buffer.length) {
28
+ offset += 1 + compressionMethodsLength; // Skip compression methods
29
+ if (offset + 2 > buffer.length)
44
30
  return undefined;
45
- }
46
31
  const extensionsLength = buffer.readUInt16BE(offset);
47
32
  offset += 2;
48
33
  const extensionsEnd = offset + extensionsLength;
49
- // Iterate over extensions
50
34
  while (offset + 4 <= extensionsEnd) {
51
35
  const extensionType = buffer.readUInt16BE(offset);
52
36
  const extensionLength = buffer.readUInt16BE(offset + 2);
53
37
  offset += 4;
54
- // Check for SNI extension (type 0)
55
- if (extensionType === 0x0000) {
56
- // SNI extension: first 2 bytes are the SNI list length.
57
- if (offset + 2 > buffer.length) {
38
+ if (extensionType === 0x0000) { // SNI extension
39
+ if (offset + 2 > buffer.length)
58
40
  return undefined;
59
- }
60
41
  const sniListLength = buffer.readUInt16BE(offset);
61
42
  offset += 2;
62
43
  const sniListEnd = offset + sniListLength;
63
- // Loop through the list; typically there is one entry.
64
44
  while (offset + 3 < sniListEnd) {
65
- const nameType = buffer.readUInt8(offset);
66
- offset++;
45
+ const nameType = buffer.readUInt8(offset++);
67
46
  const nameLen = buffer.readUInt16BE(offset);
68
47
  offset += 2;
69
48
  if (nameType === 0) { // host_name
70
- if (offset + nameLen > buffer.length) {
49
+ if (offset + nameLen > buffer.length)
71
50
  return undefined;
72
- }
73
- const serverName = buffer.toString('utf8', offset, offset + nameLen);
74
- return serverName;
51
+ return buffer.toString('utf8', offset, offset + nameLen);
75
52
  }
76
53
  offset += nameLen;
77
54
  }
@@ -85,93 +62,67 @@ function extractSNI(buffer) {
85
62
  }
86
63
  export class PortProxy {
87
64
  constructor(settings) {
88
- // Track active incoming connections
89
65
  this.activeConnections = new Set();
90
- // Record start times for incoming connections
91
66
  this.incomingConnectionTimes = new Map();
92
- // Record start times for outgoing connections
93
67
  this.outgoingConnectionTimes = new Map();
94
68
  this.connectionLogger = null;
95
- // Overall termination statistics
96
69
  this.terminationStats = {
97
70
  incoming: {},
98
71
  outgoing: {},
99
72
  };
100
73
  this.settings = {
101
74
  ...settings,
102
- toHost: settings.toHost || 'localhost'
75
+ toHost: settings.toHost || 'localhost',
103
76
  };
104
77
  }
105
- // Helper to update termination stats.
106
78
  incrementTerminationStat(side, reason) {
107
- if (!this.terminationStats[side][reason]) {
108
- this.terminationStats[side][reason] = 1;
109
- }
110
- else {
111
- this.terminationStats[side][reason]++;
112
- }
79
+ this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
113
80
  }
114
81
  async start() {
115
- // Adjusted cleanUpSockets: forcefully destroy both sockets if they haven't been destroyed.
116
- const cleanUpSockets = (from, to) => {
117
- if (!from.destroyed) {
118
- from.destroy();
119
- }
120
- if (to && !to.destroyed) {
121
- to.destroy();
122
- }
82
+ // Helper to forcefully destroy sockets.
83
+ const cleanUpSockets = (socketA, socketB) => {
84
+ if (!socketA.destroyed)
85
+ socketA.destroy();
86
+ if (socketB && !socketB.destroyed)
87
+ socketB.destroy();
123
88
  };
89
+ // Normalize an IP to include both IPv4 and IPv6 representations.
124
90
  const normalizeIP = (ip) => {
125
- // Handle IPv4-mapped IPv6 addresses
126
91
  if (ip.startsWith('::ffff:')) {
127
- const ipv4 = ip.slice(7); // Remove '::ffff:' prefix
92
+ const ipv4 = ip.slice(7);
128
93
  return [ip, ipv4];
129
94
  }
130
- // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
131
95
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
132
96
  return [ip, `::ffff:${ip}`];
133
97
  }
134
98
  return [ip];
135
99
  };
136
- const isAllowed = (value, patterns) => {
137
- // Expand patterns to include both IPv4 and IPv6 variants
100
+ // Check if a given IP matches any of the glob patterns.
101
+ const isAllowed = (ip, patterns) => {
102
+ const normalizedIPVariants = normalizeIP(ip);
138
103
  const expandedPatterns = patterns.flatMap(normalizeIP);
139
- // Check if any variant of the IP matches any expanded pattern
140
- return normalizeIP(value).some(ip => expandedPatterns.some(pattern => plugins.minimatch(ip, pattern)));
141
- };
142
- const findMatchingDomain = (serverName) => {
143
- return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
104
+ return normalizedIPVariants.some(ipVariant => expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern)));
144
105
  };
145
- // Create a plain net server for TLS passthrough.
106
+ // Find a matching domain config based on the SNI.
107
+ const findMatchingDomain = (serverName) => this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
146
108
  this.netServer = plugins.net.createServer((socket) => {
147
109
  const remoteIP = socket.remoteAddress || '';
148
- // Record start time for the incoming connection.
149
110
  this.activeConnections.add(socket);
150
111
  this.incomingConnectionTimes.set(socket, Date.now());
151
112
  console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`);
152
- // Flag to detect if we've received the first data chunk.
153
113
  let initialDataReceived = false;
154
- // Local termination reason trackers for each side.
155
- let incomingTermReason = null;
156
- let outgoingTermReason = null;
157
- // Immediately attach an error handler to catch early errors.
158
- socket.on('error', (err) => {
159
- if (!initialDataReceived) {
160
- console.log(`(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`);
161
- }
162
- else {
163
- console.log(`(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`);
164
- }
165
- });
166
- // Ensure cleanup happens only once.
114
+ let incomingTerminationReason = null;
115
+ let outgoingTerminationReason = null;
116
+ let targetSocket = null;
167
117
  let connectionClosed = false;
118
+ // Ensure cleanup happens only once.
168
119
  const cleanupOnce = () => {
169
120
  if (!connectionClosed) {
170
121
  connectionClosed = true;
171
- cleanUpSockets(socket, to || undefined);
122
+ cleanUpSockets(socket, targetSocket || undefined);
172
123
  this.incomingConnectionTimes.delete(socket);
173
- if (to) {
174
- this.outgoingConnectionTimes.delete(to);
124
+ if (targetSocket) {
125
+ this.outgoingConnectionTimes.delete(targetSocket);
175
126
  }
176
127
  if (this.activeConnections.has(socket)) {
177
128
  this.activeConnections.delete(socket);
@@ -179,9 +130,22 @@ export class PortProxy {
179
130
  }
180
131
  }
181
132
  };
182
- // Outgoing connection placeholder.
183
- let to = null;
184
- // Handle errors by recording termination reason and cleaning up.
133
+ // Helper to reject an incoming connection.
134
+ const rejectIncomingConnection = (reason, logMessage) => {
135
+ console.log(logMessage);
136
+ socket.end();
137
+ if (incomingTerminationReason === null) {
138
+ incomingTerminationReason = reason;
139
+ this.incrementTerminationStat('incoming', reason);
140
+ }
141
+ cleanupOnce();
142
+ };
143
+ socket.on('error', (err) => {
144
+ const errorMessage = initialDataReceived
145
+ ? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
146
+ : `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
147
+ console.log(errorMessage);
148
+ });
185
149
  const handleError = (side) => (err) => {
186
150
  const code = err.code;
187
151
  let reason = 'error';
@@ -192,73 +156,47 @@ export class PortProxy {
192
156
  else {
193
157
  console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
194
158
  }
195
- if (side === 'incoming' && incomingTermReason === null) {
196
- incomingTermReason = reason;
159
+ if (side === 'incoming' && incomingTerminationReason === null) {
160
+ incomingTerminationReason = reason;
197
161
  this.incrementTerminationStat('incoming', reason);
198
162
  }
199
- else if (side === 'outgoing' && outgoingTermReason === null) {
200
- outgoingTermReason = reason;
163
+ else if (side === 'outgoing' && outgoingTerminationReason === null) {
164
+ outgoingTerminationReason = reason;
201
165
  this.incrementTerminationStat('outgoing', reason);
202
166
  }
203
167
  cleanupOnce();
204
168
  };
205
- // Handle close events. If no termination reason was recorded, mark as "normal".
206
169
  const handleClose = (side) => () => {
207
170
  console.log(`Connection closed on ${side} side from ${remoteIP}`);
208
- if (side === 'incoming' && incomingTermReason === null) {
209
- incomingTermReason = 'normal';
171
+ if (side === 'incoming' && incomingTerminationReason === null) {
172
+ incomingTerminationReason = 'normal';
210
173
  this.incrementTerminationStat('incoming', 'normal');
211
174
  }
212
- else if (side === 'outgoing' && outgoingTermReason === null) {
213
- outgoingTermReason = 'normal';
175
+ else if (side === 'outgoing' && outgoingTerminationReason === null) {
176
+ outgoingTerminationReason = 'normal';
214
177
  this.incrementTerminationStat('outgoing', 'normal');
215
178
  }
216
179
  cleanupOnce();
217
180
  };
218
- // Setup connection, optionally accepting the initial data chunk.
219
181
  const setupConnection = (serverName, initialChunk) => {
220
- // Check if the IP is allowed by default.
221
- const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
222
- if (!isDefaultAllowed && serverName) {
182
+ const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
183
+ if (!defaultAllowed && serverName) {
223
184
  const domainConfig = findMatchingDomain(serverName);
224
185
  if (!domainConfig) {
225
- console.log(`Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
226
- socket.end();
227
- if (incomingTermReason === null) {
228
- incomingTermReason = 'rejected';
229
- this.incrementTerminationStat('incoming', 'rejected');
230
- }
231
- cleanupOnce();
232
- return;
186
+ return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
233
187
  }
234
188
  if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
235
- console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
236
- socket.end();
237
- if (incomingTermReason === null) {
238
- incomingTermReason = 'rejected';
239
- this.incrementTerminationStat('incoming', 'rejected');
240
- }
241
- cleanupOnce();
242
- return;
189
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
243
190
  }
244
191
  }
245
- else if (!isDefaultAllowed && !serverName) {
246
- console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
247
- socket.end();
248
- if (incomingTermReason === null) {
249
- incomingTermReason = 'rejected';
250
- this.incrementTerminationStat('incoming', 'rejected');
251
- }
252
- cleanupOnce();
253
- return;
192
+ else if (!defaultAllowed && !serverName) {
193
+ return rejectIncomingConnection('rejected', `Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
254
194
  }
255
- else {
195
+ else if (defaultAllowed && !serverName) {
256
196
  console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
257
197
  }
258
- // Determine target host.
259
198
  const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
260
199
  const targetHost = domainConfig?.targetIP || this.settings.toHost;
261
- // Create connection options.
262
200
  const connectionOptions = {
263
201
  host: targetHost,
264
202
  port: this.settings.toPort,
@@ -266,46 +204,49 @@ export class PortProxy {
266
204
  if (this.settings.preserveSourceIP) {
267
205
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
268
206
  }
269
- // Establish outgoing connection.
270
- to = plugins.net.connect(connectionOptions);
271
- if (to) {
272
- this.outgoingConnectionTimes.set(to, Date.now());
207
+ targetSocket = plugins.net.connect(connectionOptions);
208
+ if (targetSocket) {
209
+ this.outgoingConnectionTimes.set(targetSocket, Date.now());
273
210
  }
274
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
275
- // Push back the initial chunk if provided.
211
+ console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
212
+ `${serverName ? ` (SNI: ${serverName})` : ''}`);
276
213
  if (initialChunk) {
277
214
  socket.unshift(initialChunk);
278
215
  }
279
216
  socket.setTimeout(120000);
280
- socket.pipe(to);
281
- to.pipe(socket);
282
- // Attach event handlers for both sockets.
217
+ socket.pipe(targetSocket);
218
+ targetSocket.pipe(socket);
283
219
  socket.on('error', handleError('incoming'));
284
- to.on('error', handleError('outgoing'));
220
+ targetSocket.on('error', handleError('outgoing'));
285
221
  socket.on('close', handleClose('incoming'));
286
- to.on('close', handleClose('outgoing'));
222
+ targetSocket.on('close', handleClose('outgoing'));
287
223
  socket.on('timeout', () => {
288
224
  console.log(`Timeout on incoming side from ${remoteIP}`);
289
- if (incomingTermReason === null) {
290
- incomingTermReason = 'timeout';
225
+ if (incomingTerminationReason === null) {
226
+ incomingTerminationReason = 'timeout';
291
227
  this.incrementTerminationStat('incoming', 'timeout');
292
228
  }
293
229
  cleanupOnce();
294
230
  });
295
- to.on('timeout', () => {
231
+ targetSocket.on('timeout', () => {
296
232
  console.log(`Timeout on outgoing side from ${remoteIP}`);
297
- if (outgoingTermReason === null) {
298
- outgoingTermReason = 'timeout';
233
+ if (outgoingTerminationReason === null) {
234
+ outgoingTerminationReason = 'timeout';
299
235
  this.incrementTerminationStat('outgoing', 'timeout');
300
236
  }
301
237
  cleanupOnce();
302
238
  });
303
239
  socket.on('end', handleClose('incoming'));
304
- to.on('end', handleClose('outgoing'));
240
+ targetSocket.on('end', handleClose('outgoing'));
305
241
  };
306
- // For SNI-enabled connections, peek at the first chunk.
307
242
  if (this.settings.sniEnabled) {
243
+ socket.setTimeout(5000, () => {
244
+ console.log(`Initial data timeout for ${remoteIP}`);
245
+ socket.end();
246
+ cleanupOnce();
247
+ });
308
248
  socket.once('data', (chunk) => {
249
+ socket.setTimeout(0);
309
250
  initialDataReceived = true;
310
251
  const serverName = extractSNI(chunk) || '';
311
252
  console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
@@ -313,17 +254,9 @@ export class PortProxy {
313
254
  });
314
255
  }
315
256
  else {
316
- // For non-SNI connections, simply check defaultAllowedIPs.
317
257
  initialDataReceived = true;
318
258
  if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
319
- console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
320
- socket.end();
321
- if (incomingTermReason === null) {
322
- incomingTermReason = 'rejected';
323
- this.incrementTerminationStat('incoming', 'rejected');
324
- }
325
- cleanupOnce();
326
- return;
259
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
327
260
  }
328
261
  setupConnection('');
329
262
  }
@@ -332,7 +265,8 @@ export class PortProxy {
332
265
  console.log(`Server Error: ${err.message}`);
333
266
  })
334
267
  .listen(this.settings.fromPort, () => {
335
- console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
268
+ console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}` +
269
+ `${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
336
270
  });
337
271
  // Log active connection count, longest running connection durations,
338
272
  // and termination statistics every 10 seconds.
@@ -340,19 +274,16 @@ export class PortProxy {
340
274
  const now = Date.now();
341
275
  let maxIncoming = 0;
342
276
  for (const startTime of this.incomingConnectionTimes.values()) {
343
- const duration = now - startTime;
344
- if (duration > maxIncoming) {
345
- maxIncoming = duration;
346
- }
277
+ maxIncoming = Math.max(maxIncoming, now - startTime);
347
278
  }
348
279
  let maxOutgoing = 0;
349
280
  for (const startTime of this.outgoingConnectionTimes.values()) {
350
- const duration = now - startTime;
351
- if (duration > maxOutgoing) {
352
- maxOutgoing = duration;
353
- }
281
+ maxOutgoing = Math.max(maxOutgoing, now - startTime);
354
282
  }
355
- console.log(`(Interval Log) Active connections: ${this.activeConnections.size}. Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, (outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`);
283
+ console.log(`(Interval Log) Active connections: ${this.activeConnections.size}. ` +
284
+ `Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. ` +
285
+ `Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, ` +
286
+ `(outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`);
356
287
  }, 10000);
357
288
  }
358
289
  async stop() {
@@ -367,4 +298,4 @@ export class PortProxy {
367
298
  await done.promise;
368
299
  }
369
300
  }
370
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwcm94eS5wb3J0cHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHByb3h5LnBvcnRwcm94eS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQXFCeEM7OztHQUdHO0FBQ0gsU0FBUyxVQUFVLENBQUMsTUFBYztJQUNoQyxJQUFJLE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDZixrREFBa0Q7SUFDbEQsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3RCLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFFRCxvQkFBb0I7SUFDcEIsTUFBTSxVQUFVLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN2QyxJQUFJLFVBQVUsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDLGlCQUFpQjtRQUN4QyxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBQ0QscUJBQXFCO0lBQ3JCLE1BQU0sWUFBWSxHQUFHLE1BQU0sQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDNUMsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsR0FBRyxZQUFZLEVBQUUsQ0FBQztRQUNyQyxrRkFBa0Y7UUFDbEYsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUVELE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDWCxzREFBc0Q7SUFDdEQsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUMvQyxJQUFJLGFBQWEsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUN4QixPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBQ0QsdURBQXVEO0lBQ3ZELE1BQU0sSUFBSSxDQUFDLENBQUM7SUFFWixzREFBc0Q7SUFDdEQsTUFBTSxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7SUFFakIsYUFBYTtJ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
301
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwcm94eS5wb3J0cHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHByb3h5LnBvcnRwcm94eS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQWtCeEM7Ozs7R0FJRztBQUNILFNBQVMsVUFBVSxDQUFDLE1BQWM7SUFDaEMsSUFBSSxNQUFNLEdBQUcsQ0FBQyxDQUFDO0lBQ2YsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUM7UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUV4QyxNQUFNLFVBQVUsR0FBRyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3ZDLElBQUksVUFBVSxLQUFLLEVBQUU7UUFBRSxPQUFPLFNBQVMsQ0FBQyxDQUFDLGlCQUFpQjtJQUUxRCxNQUFNLFlBQVksR0FBRyxNQUFNLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQzVDLElBQUksTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDLEdBQUcsWUFBWTtRQUFFLE9BQU8sU0FBUyxDQUFDO0lBRXZELE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDWCxNQUFNLGFBQWEsR0FBRyxNQUFNLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQy9DLElBQUksYUFBYSxLQUFLLENBQUM7UUFBRSxPQUFPLFNBQVMsQ0FBQyxDQUFDLGtCQUFrQjtJQUU3RCxNQUFNLElBQUksQ0FBQyxDQUFDLENBQUMsd0NBQXdDO0lBQ3JELE1BQU0sSUFBSSxDQUFDLEdBQUcsRUFBRSxDQUFDLENBQUMsaUNBQWlDO0lBRW5ELE1BQU0sZUFBZSxHQUFHLE1BQU0sQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLENBQUM7SUFDakQsTUFBTSxJQUFJLENBQUMsR0FBRyxlQUFlLENBQUMsQ0FBQyxrQkFBa0I7SUFFakQsTUFBTSxrQkFBa0IsR0FBRyxNQUFNLENBQUMsWUFBWSxDQUFDLE1B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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartproxy",
3
- "version": "3.10.1",
3
+ "version": "3.10.3",
4
4
  "private": false,
5
5
  "description": "a proxy for handling high workloads of proxying",
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.10.1',
6
+ version: '3.10.3',
7
7
  description: 'a proxy for handling high workloads of proxying'
8
8
  }
@@ -1,106 +1,73 @@
1
1
  import * as plugins from './plugins.js';
2
2
 
3
3
  export interface IDomainConfig {
4
- domain: string; // glob pattern for domain
5
- allowedIPs: string[]; // glob patterns for IPs allowed to access this domain
4
+ domain: string; // Glob pattern for domain
5
+ allowedIPs: string[]; // Glob patterns for allowed IPs
6
6
  targetIP?: string; // Optional target IP for this domain
7
7
  }
8
8
 
9
9
  export interface IProxySettings extends plugins.tls.TlsOptions {
10
- // Port configuration
11
10
  fromPort: number;
12
11
  toPort: number;
13
- toHost?: string; // Target host to proxy to, defaults to 'localhost'
14
-
15
- // Domain and security settings
12
+ toHost?: string; // Target host to proxy to, defaults to 'localhost'
16
13
  domains: IDomainConfig[];
17
14
  sniEnabled?: boolean;
18
- defaultAllowedIPs?: string[]; // Optional default IP patterns if no matching domain found
19
- preserveSourceIP?: boolean; // Whether to preserve the client's source IP when proxying
15
+ defaultAllowedIPs?: string[];
16
+ preserveSourceIP?: boolean;
20
17
  }
21
18
 
22
19
  /**
23
- * Extract SNI (Server Name Indication) from a TLS ClientHello packet.
24
- * Returns the server name if found, or undefined.
20
+ * Extracts the SNI (Server Name Indication) from a TLS ClientHello packet.
21
+ * @param buffer - Buffer containing the TLS ClientHello.
22
+ * @returns The server name if found, otherwise undefined.
25
23
  */
26
24
  function extractSNI(buffer: Buffer): string | undefined {
27
25
  let offset = 0;
28
- // We need at least 5 bytes for the record header.
29
- if (buffer.length < 5) {
30
- return undefined;
31
- }
26
+ if (buffer.length < 5) return undefined;
32
27
 
33
- // TLS record header
34
28
  const recordType = buffer.readUInt8(0);
35
- if (recordType !== 22) { // 22 = handshake
36
- return undefined;
37
- }
38
- // Read record length
29
+ if (recordType !== 22) return undefined; // 22 = handshake
30
+
39
31
  const recordLength = buffer.readUInt16BE(3);
40
- if (buffer.length < 5 + recordLength) {
41
- // Not all data arrived yet; in production you might need to accumulate more data.
42
- return undefined;
43
- }
32
+ if (buffer.length < 5 + recordLength) return undefined;
44
33
 
45
34
  offset = 5;
46
- // Handshake message type should be 1 for ClientHello.
47
35
  const handshakeType = buffer.readUInt8(offset);
48
- if (handshakeType !== 1) {
49
- return undefined;
50
- }
51
- // Skip handshake header (1 byte type + 3 bytes length)
52
- offset += 4;
36
+ if (handshakeType !== 1) return undefined; // 1 = ClientHello
53
37
 
54
- // Skip client version (2 bytes) and random (32 bytes)
55
- offset += 2 + 32;
38
+ offset += 4; // Skip handshake header (type + length)
39
+ offset += 2 + 32; // Skip client version and random
56
40
 
57
- // Session ID
58
41
  const sessionIDLength = buffer.readUInt8(offset);
59
- offset += 1 + sessionIDLength;
42
+ offset += 1 + sessionIDLength; // Skip session ID
60
43
 
61
- // Cipher suites
62
44
  const cipherSuitesLength = buffer.readUInt16BE(offset);
63
- offset += 2 + cipherSuitesLength;
45
+ offset += 2 + cipherSuitesLength; // Skip cipher suites
64
46
 
65
- // Compression methods
66
47
  const compressionMethodsLength = buffer.readUInt8(offset);
67
- offset += 1 + compressionMethodsLength;
48
+ offset += 1 + compressionMethodsLength; // Skip compression methods
68
49
 
69
- // Extensions length
70
- if (offset + 2 > buffer.length) {
71
- return undefined;
72
- }
50
+ if (offset + 2 > buffer.length) return undefined;
73
51
  const extensionsLength = buffer.readUInt16BE(offset);
74
52
  offset += 2;
75
53
  const extensionsEnd = offset + extensionsLength;
76
54
 
77
- // Iterate over extensions
78
55
  while (offset + 4 <= extensionsEnd) {
79
56
  const extensionType = buffer.readUInt16BE(offset);
80
57
  const extensionLength = buffer.readUInt16BE(offset + 2);
81
58
  offset += 4;
82
-
83
- // Check for SNI extension (type 0)
84
- if (extensionType === 0x0000) {
85
- // SNI extension: first 2 bytes are the SNI list length.
86
- if (offset + 2 > buffer.length) {
87
- return undefined;
88
- }
59
+ if (extensionType === 0x0000) { // SNI extension
60
+ if (offset + 2 > buffer.length) return undefined;
89
61
  const sniListLength = buffer.readUInt16BE(offset);
90
62
  offset += 2;
91
63
  const sniListEnd = offset + sniListLength;
92
- // Loop through the list; typically there is one entry.
93
64
  while (offset + 3 < sniListEnd) {
94
- const nameType = buffer.readUInt8(offset);
95
- offset++;
65
+ const nameType = buffer.readUInt8(offset++);
96
66
  const nameLen = buffer.readUInt16BE(offset);
97
67
  offset += 2;
98
68
  if (nameType === 0) { // host_name
99
- if (offset + nameLen > buffer.length) {
100
- return undefined;
101
- }
102
- const serverName = buffer.toString('utf8', offset, offset + nameLen);
103
- return serverName;
69
+ if (offset + nameLen > buffer.length) return undefined;
70
+ return buffer.toString('utf8', offset, offset + nameLen);
104
71
  }
105
72
  offset += nameLen;
106
73
  }
@@ -115,15 +82,11 @@ function extractSNI(buffer: Buffer): string | undefined {
115
82
  export class PortProxy {
116
83
  netServer: plugins.net.Server;
117
84
  settings: IProxySettings;
118
- // Track active incoming connections
119
85
  private activeConnections: Set<plugins.net.Socket> = new Set();
120
- // Record start times for incoming connections
121
86
  private incomingConnectionTimes: Map<plugins.net.Socket, number> = new Map();
122
- // Record start times for outgoing connections
123
87
  private outgoingConnectionTimes: Map<plugins.net.Socket, number> = new Map();
124
88
  private connectionLogger: NodeJS.Timeout | null = null;
125
89
 
126
- // Overall termination statistics
127
90
  private terminationStats: {
128
91
  incoming: Record<string, number>;
129
92
  outgoing: Record<string, number>;
@@ -135,90 +98,66 @@ export class PortProxy {
135
98
  constructor(settings: IProxySettings) {
136
99
  this.settings = {
137
100
  ...settings,
138
- toHost: settings.toHost || 'localhost'
101
+ toHost: settings.toHost || 'localhost',
139
102
  };
140
103
  }
141
104
 
142
- // Helper to update termination stats.
143
105
  private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
144
- if (!this.terminationStats[side][reason]) {
145
- this.terminationStats[side][reason] = 1;
146
- } else {
147
- this.terminationStats[side][reason]++;
148
- }
106
+ this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
149
107
  }
150
108
 
151
109
  public async start() {
152
- // Adjusted cleanUpSockets: forcefully destroy both sockets if they haven't been destroyed.
153
- const cleanUpSockets = (from: plugins.net.Socket, to?: plugins.net.Socket) => {
154
- if (!from.destroyed) {
155
- from.destroy();
156
- }
157
- if (to && !to.destroyed) {
158
- to.destroy();
159
- }
110
+ // Helper to forcefully destroy sockets.
111
+ const cleanUpSockets = (socketA: plugins.net.Socket, socketB?: plugins.net.Socket) => {
112
+ if (!socketA.destroyed) socketA.destroy();
113
+ if (socketB && !socketB.destroyed) socketB.destroy();
160
114
  };
161
115
 
116
+ // Normalize an IP to include both IPv4 and IPv6 representations.
162
117
  const normalizeIP = (ip: string): string[] => {
163
- // Handle IPv4-mapped IPv6 addresses
164
118
  if (ip.startsWith('::ffff:')) {
165
- const ipv4 = ip.slice(7); // Remove '::ffff:' prefix
119
+ const ipv4 = ip.slice(7);
166
120
  return [ip, ipv4];
167
121
  }
168
- // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
169
122
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
170
123
  return [ip, `::ffff:${ip}`];
171
124
  }
172
125
  return [ip];
173
126
  };
174
127
 
175
- const isAllowed = (value: string, patterns: string[]): boolean => {
176
- // Expand patterns to include both IPv4 and IPv6 variants
128
+ // Check if a given IP matches any of the glob patterns.
129
+ const isAllowed = (ip: string, patterns: string[]): boolean => {
130
+ const normalizedIPVariants = normalizeIP(ip);
177
131
  const expandedPatterns = patterns.flatMap(normalizeIP);
178
- // Check if any variant of the IP matches any expanded pattern
179
- return normalizeIP(value).some(ip =>
180
- expandedPatterns.some(pattern => plugins.minimatch(ip, pattern))
132
+ return normalizedIPVariants.some(ipVariant =>
133
+ expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
181
134
  );
182
135
  };
183
136
 
184
- const findMatchingDomain = (serverName: string): IDomainConfig | undefined => {
185
- return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
186
- };
137
+ // Find a matching domain config based on the SNI.
138
+ const findMatchingDomain = (serverName: string): IDomainConfig | undefined =>
139
+ this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
187
140
 
188
- // Create a plain net server for TLS passthrough.
189
141
  this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
190
142
  const remoteIP = socket.remoteAddress || '';
191
-
192
- // Record start time for the incoming connection.
193
143
  this.activeConnections.add(socket);
194
144
  this.incomingConnectionTimes.set(socket, Date.now());
195
145
  console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`);
196
146
 
197
- // Flag to detect if we've received the first data chunk.
198
147
  let initialDataReceived = false;
199
-
200
- // Local termination reason trackers for each side.
201
- let incomingTermReason: string | null = null;
202
- let outgoingTermReason: string | null = null;
203
-
204
- // Immediately attach an error handler to catch early errors.
205
- socket.on('error', (err: Error) => {
206
- if (!initialDataReceived) {
207
- console.log(`(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`);
208
- } else {
209
- console.log(`(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`);
210
- }
211
- });
148
+ let incomingTerminationReason: string | null = null;
149
+ let outgoingTerminationReason: string | null = null;
150
+ let targetSocket: plugins.net.Socket | null = null;
151
+ let connectionClosed = false;
212
152
 
213
153
  // Ensure cleanup happens only once.
214
- let connectionClosed = false;
215
154
  const cleanupOnce = () => {
216
155
  if (!connectionClosed) {
217
156
  connectionClosed = true;
218
- cleanUpSockets(socket, to || undefined);
157
+ cleanUpSockets(socket, targetSocket || undefined);
219
158
  this.incomingConnectionTimes.delete(socket);
220
- if (to) {
221
- this.outgoingConnectionTimes.delete(to);
159
+ if (targetSocket) {
160
+ this.outgoingConnectionTimes.delete(targetSocket);
222
161
  }
223
162
  if (this.activeConnections.has(socket)) {
224
163
  this.activeConnections.delete(socket);
@@ -227,10 +166,24 @@ export class PortProxy {
227
166
  }
228
167
  };
229
168
 
230
- // Outgoing connection placeholder.
231
- let to: plugins.net.Socket | null = null;
169
+ // Helper to reject an incoming connection.
170
+ const rejectIncomingConnection = (reason: string, logMessage: string) => {
171
+ console.log(logMessage);
172
+ socket.end();
173
+ if (incomingTerminationReason === null) {
174
+ incomingTerminationReason = reason;
175
+ this.incrementTerminationStat('incoming', reason);
176
+ }
177
+ cleanupOnce();
178
+ };
179
+
180
+ socket.on('error', (err: Error) => {
181
+ const errorMessage = initialDataReceived
182
+ ? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
183
+ : `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
184
+ console.log(errorMessage);
185
+ });
232
186
 
233
- // Handle errors by recording termination reason and cleaning up.
234
187
  const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
235
188
  const code = (err as any).code;
236
189
  let reason = 'error';
@@ -240,73 +193,47 @@ export class PortProxy {
240
193
  } else {
241
194
  console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
242
195
  }
243
- if (side === 'incoming' && incomingTermReason === null) {
244
- incomingTermReason = reason;
196
+ if (side === 'incoming' && incomingTerminationReason === null) {
197
+ incomingTerminationReason = reason;
245
198
  this.incrementTerminationStat('incoming', reason);
246
- } else if (side === 'outgoing' && outgoingTermReason === null) {
247
- outgoingTermReason = reason;
199
+ } else if (side === 'outgoing' && outgoingTerminationReason === null) {
200
+ outgoingTerminationReason = reason;
248
201
  this.incrementTerminationStat('outgoing', reason);
249
202
  }
250
203
  cleanupOnce();
251
204
  };
252
205
 
253
- // Handle close events. If no termination reason was recorded, mark as "normal".
254
206
  const handleClose = (side: 'incoming' | 'outgoing') => () => {
255
207
  console.log(`Connection closed on ${side} side from ${remoteIP}`);
256
- if (side === 'incoming' && incomingTermReason === null) {
257
- incomingTermReason = 'normal';
208
+ if (side === 'incoming' && incomingTerminationReason === null) {
209
+ incomingTerminationReason = 'normal';
258
210
  this.incrementTerminationStat('incoming', 'normal');
259
- } else if (side === 'outgoing' && outgoingTermReason === null) {
260
- outgoingTermReason = 'normal';
211
+ } else if (side === 'outgoing' && outgoingTerminationReason === null) {
212
+ outgoingTerminationReason = 'normal';
261
213
  this.incrementTerminationStat('outgoing', 'normal');
262
214
  }
263
215
  cleanupOnce();
264
216
  };
265
217
 
266
- // Setup connection, optionally accepting the initial data chunk.
267
218
  const setupConnection = (serverName: string, initialChunk?: Buffer) => {
268
- // Check if the IP is allowed by default.
269
- const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
270
- if (!isDefaultAllowed && serverName) {
219
+ const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
220
+
221
+ if (!defaultAllowed && serverName) {
271
222
  const domainConfig = findMatchingDomain(serverName);
272
223
  if (!domainConfig) {
273
- console.log(`Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
274
- socket.end();
275
- if (incomingTermReason === null) {
276
- incomingTermReason = 'rejected';
277
- this.incrementTerminationStat('incoming', 'rejected');
278
- }
279
- cleanupOnce();
280
- return;
224
+ return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
281
225
  }
282
226
  if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
283
- console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
284
- socket.end();
285
- if (incomingTermReason === null) {
286
- incomingTermReason = 'rejected';
287
- this.incrementTerminationStat('incoming', 'rejected');
288
- }
289
- cleanupOnce();
290
- return;
227
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
291
228
  }
292
- } else if (!isDefaultAllowed && !serverName) {
293
- console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
294
- socket.end();
295
- if (incomingTermReason === null) {
296
- incomingTermReason = 'rejected';
297
- this.incrementTerminationStat('incoming', 'rejected');
298
- }
299
- cleanupOnce();
300
- return;
301
- } else {
229
+ } else if (!defaultAllowed && !serverName) {
230
+ return rejectIncomingConnection('rejected', `Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
231
+ } else if (defaultAllowed && !serverName) {
302
232
  console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
303
233
  }
304
234
 
305
- // Determine target host.
306
235
  const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
307
236
  const targetHost = domainConfig?.targetIP || this.settings.toHost!;
308
-
309
- // Create connection options.
310
237
  const connectionOptions: plugins.net.NetConnectOpts = {
311
238
  host: targetHost,
312
239
  port: this.settings.toPort,
@@ -315,76 +242,77 @@ export class PortProxy {
315
242
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
316
243
  }
317
244
 
318
- // Establish outgoing connection.
319
- to = plugins.net.connect(connectionOptions);
320
- if (to) {
321
- this.outgoingConnectionTimes.set(to, Date.now());
245
+ targetSocket = plugins.net.connect(connectionOptions);
246
+ if (targetSocket) {
247
+ this.outgoingConnectionTimes.set(targetSocket, Date.now());
322
248
  }
323
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
324
-
325
- // Push back the initial chunk if provided.
249
+ console.log(
250
+ `Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
251
+ `${serverName ? ` (SNI: ${serverName})` : ''}`
252
+ );
253
+
326
254
  if (initialChunk) {
327
255
  socket.unshift(initialChunk);
328
256
  }
329
257
  socket.setTimeout(120000);
330
- socket.pipe(to!);
331
- to!.pipe(socket);
258
+ socket.pipe(targetSocket);
259
+ targetSocket.pipe(socket);
332
260
 
333
- // Attach event handlers for both sockets.
334
261
  socket.on('error', handleError('incoming'));
335
- to!.on('error', handleError('outgoing'));
262
+ targetSocket.on('error', handleError('outgoing'));
336
263
  socket.on('close', handleClose('incoming'));
337
- to!.on('close', handleClose('outgoing'));
264
+ targetSocket.on('close', handleClose('outgoing'));
338
265
  socket.on('timeout', () => {
339
266
  console.log(`Timeout on incoming side from ${remoteIP}`);
340
- if (incomingTermReason === null) {
341
- incomingTermReason = 'timeout';
267
+ if (incomingTerminationReason === null) {
268
+ incomingTerminationReason = 'timeout';
342
269
  this.incrementTerminationStat('incoming', 'timeout');
343
270
  }
344
271
  cleanupOnce();
345
272
  });
346
- to!.on('timeout', () => {
273
+ targetSocket.on('timeout', () => {
347
274
  console.log(`Timeout on outgoing side from ${remoteIP}`);
348
- if (outgoingTermReason === null) {
349
- outgoingTermReason = 'timeout';
275
+ if (outgoingTerminationReason === null) {
276
+ outgoingTerminationReason = 'timeout';
350
277
  this.incrementTerminationStat('outgoing', 'timeout');
351
278
  }
352
279
  cleanupOnce();
353
280
  });
354
281
  socket.on('end', handleClose('incoming'));
355
- to!.on('end', handleClose('outgoing'));
282
+ targetSocket.on('end', handleClose('outgoing'));
356
283
  };
357
284
 
358
- // For SNI-enabled connections, peek at the first chunk.
359
285
  if (this.settings.sniEnabled) {
286
+ socket.setTimeout(5000, () => {
287
+ console.log(`Initial data timeout for ${remoteIP}`);
288
+ socket.end();
289
+ cleanupOnce();
290
+ });
291
+
360
292
  socket.once('data', (chunk: Buffer) => {
293
+ socket.setTimeout(0);
361
294
  initialDataReceived = true;
362
295
  const serverName = extractSNI(chunk) || '';
363
296
  console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
364
297
  setupConnection(serverName, chunk);
365
298
  });
366
299
  } else {
367
- // For non-SNI connections, simply check defaultAllowedIPs.
368
300
  initialDataReceived = true;
369
301
  if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
370
- console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
371
- socket.end();
372
- if (incomingTermReason === null) {
373
- incomingTermReason = 'rejected';
374
- this.incrementTerminationStat('incoming', 'rejected');
375
- }
376
- cleanupOnce();
377
- return;
302
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
378
303
  }
379
304
  setupConnection('');
380
305
  }
381
306
  })
382
- .on('error', (err: Error) => {
383
- console.log(`Server Error: ${err.message}`);
384
- })
385
- .listen(this.settings.fromPort, () => {
386
- console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
387
- });
307
+ .on('error', (err: Error) => {
308
+ console.log(`Server Error: ${err.message}`);
309
+ })
310
+ .listen(this.settings.fromPort, () => {
311
+ console.log(
312
+ `PortProxy -> OK: Now listening on port ${this.settings.fromPort}` +
313
+ `${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`
314
+ );
315
+ });
388
316
 
389
317
  // Log active connection count, longest running connection durations,
390
318
  // and termination statistics every 10 seconds.
@@ -392,19 +320,18 @@ export class PortProxy {
392
320
  const now = Date.now();
393
321
  let maxIncoming = 0;
394
322
  for (const startTime of this.incomingConnectionTimes.values()) {
395
- const duration = now - startTime;
396
- if (duration > maxIncoming) {
397
- maxIncoming = duration;
398
- }
323
+ maxIncoming = Math.max(maxIncoming, now - startTime);
399
324
  }
400
325
  let maxOutgoing = 0;
401
326
  for (const startTime of this.outgoingConnectionTimes.values()) {
402
- const duration = now - startTime;
403
- if (duration > maxOutgoing) {
404
- maxOutgoing = duration;
405
- }
327
+ maxOutgoing = Math.max(maxOutgoing, now - startTime);
406
328
  }
407
- console.log(`(Interval Log) Active connections: ${this.activeConnections.size}. Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, (outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`);
329
+ console.log(
330
+ `(Interval Log) Active connections: ${this.activeConnections.size}. ` +
331
+ `Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. ` +
332
+ `Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, ` +
333
+ `(outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`
334
+ );
408
335
  }, 10000);
409
336
  }
410
337