@push.rocks/smartproxy 3.10.2 → 3.10.4

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.2',
6
+ version: '3.10.4',
7
7
  description: 'a proxy for handling high workloads of proxying'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLGlEQUFpRDtDQUMvRCxDQUFBIn0=
@@ -16,9 +16,7 @@ export interface IProxySettings extends plugins.tls.TlsOptions {
16
16
  export declare class PortProxy {
17
17
  netServer: plugins.net.Server;
18
18
  settings: IProxySettings;
19
- private activeConnections;
20
- private incomingConnectionTimes;
21
- private outgoingConnectionTimes;
19
+ private connectionRecords;
22
20
  private connectionLogger;
23
21
  private terminationStats;
24
22
  constructor(settings: IProxySettings);
@@ -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,103 +62,86 @@ function extractSNI(buffer) {
85
62
  }
86
63
  export class PortProxy {
87
64
  constructor(settings) {
88
- // Track active incoming connections
89
- this.activeConnections = new Set();
90
- // Record start times for incoming connections
91
- this.incomingConnectionTimes = new Map();
92
- // Record start times for outgoing connections
93
- this.outgoingConnectionTimes = new Map();
65
+ // Unified record tracking each connection pair.
66
+ this.connectionRecords = new Set();
94
67
  this.connectionLogger = null;
95
- // Overall termination statistics
96
68
  this.terminationStats = {
97
69
  incoming: {},
98
70
  outgoing: {},
99
71
  };
100
72
  this.settings = {
101
73
  ...settings,
102
- toHost: settings.toHost || 'localhost'
74
+ toHost: settings.toHost || 'localhost',
103
75
  };
104
76
  }
105
- // Helper to update termination stats.
106
77
  incrementTerminationStat(side, reason) {
107
- if (!this.terminationStats[side][reason]) {
108
- this.terminationStats[side][reason] = 1;
109
- }
110
- else {
111
- this.terminationStats[side][reason]++;
112
- }
78
+ this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
113
79
  }
114
80
  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
- }
81
+ // Helper to forcefully destroy sockets.
82
+ const cleanUpSockets = (socketA, socketB) => {
83
+ if (!socketA.destroyed)
84
+ socketA.destroy();
85
+ if (socketB && !socketB.destroyed)
86
+ socketB.destroy();
123
87
  };
88
+ // Normalize an IP to include both IPv4 and IPv6 representations.
124
89
  const normalizeIP = (ip) => {
125
- // Handle IPv4-mapped IPv6 addresses
126
90
  if (ip.startsWith('::ffff:')) {
127
- const ipv4 = ip.slice(7); // Remove '::ffff:' prefix
91
+ const ipv4 = ip.slice(7);
128
92
  return [ip, ipv4];
129
93
  }
130
- // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
131
94
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
132
95
  return [ip, `::ffff:${ip}`];
133
96
  }
134
97
  return [ip];
135
98
  };
136
- const isAllowed = (value, patterns) => {
137
- // Expand patterns to include both IPv4 and IPv6 variants
99
+ // Check if a given IP matches any of the glob patterns.
100
+ const isAllowed = (ip, patterns) => {
101
+ const normalizedIPVariants = normalizeIP(ip);
138
102
  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));
103
+ return normalizedIPVariants.some(ipVariant => expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern)));
144
104
  };
145
- // Create a plain net server for TLS passthrough.
105
+ // Find a matching domain config based on the SNI.
106
+ const findMatchingDomain = (serverName) => this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
146
107
  this.netServer = plugins.net.createServer((socket) => {
147
108
  const remoteIP = socket.remoteAddress || '';
148
- // Record start time for the incoming connection.
149
- this.activeConnections.add(socket);
150
- this.incomingConnectionTimes.set(socket, Date.now());
151
- console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`);
152
- // Flag to detect if we've received the first data chunk.
109
+ const connectionRecord = {
110
+ incoming: socket,
111
+ outgoing: null,
112
+ incomingStartTime: Date.now(),
113
+ connectionClosed: false,
114
+ };
115
+ this.connectionRecords.add(connectionRecord);
116
+ console.log(`New connection from ${remoteIP}. Active connections: ${this.connectionRecords.size}`);
153
117
  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.
167
- let connectionClosed = false;
118
+ let incomingTerminationReason = null;
119
+ let outgoingTerminationReason = null;
120
+ // Ensure cleanup happens only once for the entire connection record.
168
121
  const cleanupOnce = () => {
169
- if (!connectionClosed) {
170
- connectionClosed = true;
171
- cleanUpSockets(socket, to || undefined);
172
- this.incomingConnectionTimes.delete(socket);
173
- if (to) {
174
- this.outgoingConnectionTimes.delete(to);
175
- }
176
- if (this.activeConnections.has(socket)) {
177
- this.activeConnections.delete(socket);
178
- console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.activeConnections.size}`);
179
- }
122
+ if (!connectionRecord.connectionClosed) {
123
+ connectionRecord.connectionClosed = true;
124
+ cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined);
125
+ this.connectionRecords.delete(connectionRecord);
126
+ console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
127
+ }
128
+ };
129
+ // Helper to reject an incoming connection.
130
+ const rejectIncomingConnection = (reason, logMessage) => {
131
+ console.log(logMessage);
132
+ socket.end();
133
+ if (incomingTerminationReason === null) {
134
+ incomingTerminationReason = reason;
135
+ this.incrementTerminationStat('incoming', reason);
180
136
  }
137
+ cleanupOnce();
181
138
  };
182
- // Outgoing connection placeholder.
183
- let to = null;
184
- // Handle errors by recording termination reason and cleaning up.
139
+ socket.on('error', (err) => {
140
+ const errorMessage = initialDataReceived
141
+ ? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
142
+ : `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
143
+ console.log(errorMessage);
144
+ });
185
145
  const handleError = (side) => (err) => {
186
146
  const code = err.code;
187
147
  let reason = 'error';
@@ -192,73 +152,47 @@ export class PortProxy {
192
152
  else {
193
153
  console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
194
154
  }
195
- if (side === 'incoming' && incomingTermReason === null) {
196
- incomingTermReason = reason;
155
+ if (side === 'incoming' && incomingTerminationReason === null) {
156
+ incomingTerminationReason = reason;
197
157
  this.incrementTerminationStat('incoming', reason);
198
158
  }
199
- else if (side === 'outgoing' && outgoingTermReason === null) {
200
- outgoingTermReason = reason;
159
+ else if (side === 'outgoing' && outgoingTerminationReason === null) {
160
+ outgoingTerminationReason = reason;
201
161
  this.incrementTerminationStat('outgoing', reason);
202
162
  }
203
163
  cleanupOnce();
204
164
  };
205
- // Handle close events. If no termination reason was recorded, mark as "normal".
206
165
  const handleClose = (side) => () => {
207
166
  console.log(`Connection closed on ${side} side from ${remoteIP}`);
208
- if (side === 'incoming' && incomingTermReason === null) {
209
- incomingTermReason = 'normal';
167
+ if (side === 'incoming' && incomingTerminationReason === null) {
168
+ incomingTerminationReason = 'normal';
210
169
  this.incrementTerminationStat('incoming', 'normal');
211
170
  }
212
- else if (side === 'outgoing' && outgoingTermReason === null) {
213
- outgoingTermReason = 'normal';
171
+ else if (side === 'outgoing' && outgoingTerminationReason === null) {
172
+ outgoingTerminationReason = 'normal';
214
173
  this.incrementTerminationStat('outgoing', 'normal');
215
174
  }
216
175
  cleanupOnce();
217
176
  };
218
- // Setup connection, optionally accepting the initial data chunk.
219
177
  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) {
178
+ const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
179
+ if (!defaultAllowed && serverName) {
223
180
  const domainConfig = findMatchingDomain(serverName);
224
181
  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;
182
+ return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
233
183
  }
234
184
  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;
185
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
243
186
  }
244
187
  }
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;
188
+ else if (!defaultAllowed && !serverName) {
189
+ return rejectIncomingConnection('rejected', `Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
254
190
  }
255
- else {
191
+ else if (defaultAllowed && !serverName) {
256
192
  console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
257
193
  }
258
- // Determine target host.
259
194
  const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
260
195
  const targetHost = domainConfig?.targetIP || this.settings.toHost;
261
- // Create connection options.
262
196
  const connectionOptions = {
263
197
  host: targetHost,
264
198
  port: this.settings.toPort,
@@ -266,53 +200,47 @@ export class PortProxy {
266
200
  if (this.settings.preserveSourceIP) {
267
201
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
268
202
  }
269
- // Establish outgoing connection.
270
- to = plugins.net.connect(connectionOptions);
271
- if (to) {
272
- this.outgoingConnectionTimes.set(to, Date.now());
273
- }
274
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
275
- // Push back the initial chunk if provided.
203
+ const targetSocket = plugins.net.connect(connectionOptions);
204
+ connectionRecord.outgoing = targetSocket;
205
+ connectionRecord.outgoingStartTime = Date.now();
206
+ console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
207
+ `${serverName ? ` (SNI: ${serverName})` : ''}`);
276
208
  if (initialChunk) {
277
209
  socket.unshift(initialChunk);
278
210
  }
279
211
  socket.setTimeout(120000);
280
- socket.pipe(to);
281
- to.pipe(socket);
282
- // Attach event handlers for both sockets.
212
+ socket.pipe(targetSocket);
213
+ targetSocket.pipe(socket);
283
214
  socket.on('error', handleError('incoming'));
284
- to.on('error', handleError('outgoing'));
215
+ targetSocket.on('error', handleError('outgoing'));
285
216
  socket.on('close', handleClose('incoming'));
286
- to.on('close', handleClose('outgoing'));
217
+ targetSocket.on('close', handleClose('outgoing'));
287
218
  socket.on('timeout', () => {
288
219
  console.log(`Timeout on incoming side from ${remoteIP}`);
289
- if (incomingTermReason === null) {
290
- incomingTermReason = 'timeout';
220
+ if (incomingTerminationReason === null) {
221
+ incomingTerminationReason = 'timeout';
291
222
  this.incrementTerminationStat('incoming', 'timeout');
292
223
  }
293
224
  cleanupOnce();
294
225
  });
295
- to.on('timeout', () => {
226
+ targetSocket.on('timeout', () => {
296
227
  console.log(`Timeout on outgoing side from ${remoteIP}`);
297
- if (outgoingTermReason === null) {
298
- outgoingTermReason = 'timeout';
228
+ if (outgoingTerminationReason === null) {
229
+ outgoingTerminationReason = 'timeout';
299
230
  this.incrementTerminationStat('outgoing', 'timeout');
300
231
  }
301
232
  cleanupOnce();
302
233
  });
303
234
  socket.on('end', handleClose('incoming'));
304
- to.on('end', handleClose('outgoing'));
235
+ targetSocket.on('end', handleClose('outgoing'));
305
236
  };
306
- // For SNI-enabled connections, set an initial data timeout before waiting for data.
307
237
  if (this.settings.sniEnabled) {
308
- // Set an initial timeout for receiving data (e.g., 5 seconds)
309
238
  socket.setTimeout(5000, () => {
310
239
  console.log(`Initial data timeout for ${remoteIP}`);
311
240
  socket.end();
312
241
  cleanupOnce();
313
242
  });
314
243
  socket.once('data', (chunk) => {
315
- // Clear the initial timeout since data has been received
316
244
  socket.setTimeout(0);
317
245
  initialDataReceived = true;
318
246
  const serverName = extractSNI(chunk) || '';
@@ -321,17 +249,9 @@ export class PortProxy {
321
249
  });
322
250
  }
323
251
  else {
324
- // For non-SNI connections, simply check defaultAllowedIPs.
325
252
  initialDataReceived = true;
326
253
  if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
327
- console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
328
- socket.end();
329
- if (incomingTermReason === null) {
330
- incomingTermReason = 'rejected';
331
- this.incrementTerminationStat('incoming', 'rejected');
332
- }
333
- cleanupOnce();
334
- return;
254
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
335
255
  }
336
256
  setupConnection('');
337
257
  }
@@ -340,27 +260,24 @@ export class PortProxy {
340
260
  console.log(`Server Error: ${err.message}`);
341
261
  })
342
262
  .listen(this.settings.fromPort, () => {
343
- console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
263
+ console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}` +
264
+ `${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
344
265
  });
345
- // Log active connection count, longest running connection durations,
346
- // and termination statistics every 10 seconds.
266
+ // Every 10 seconds log active connection count and longest running durations.
347
267
  this.connectionLogger = setInterval(() => {
348
268
  const now = Date.now();
349
269
  let maxIncoming = 0;
350
- for (const startTime of this.incomingConnectionTimes.values()) {
351
- const duration = now - startTime;
352
- if (duration > maxIncoming) {
353
- maxIncoming = duration;
354
- }
355
- }
356
270
  let maxOutgoing = 0;
357
- for (const startTime of this.outgoingConnectionTimes.values()) {
358
- const duration = now - startTime;
359
- if (duration > maxOutgoing) {
360
- maxOutgoing = duration;
271
+ for (const record of this.connectionRecords) {
272
+ maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
273
+ if (record.outgoingStartTime) {
274
+ maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
361
275
  }
362
276
  }
363
- 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)}`);
277
+ console.log(`(Interval Log) Active connections: ${this.connectionRecords.size}. ` +
278
+ `Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. ` +
279
+ `Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, ` +
280
+ `(outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`);
364
281
  }, 10000);
365
282
  }
366
283
  async stop() {
@@ -375,4 +292,4 @@ export class PortProxy {
375
292
  await done.promise;
376
293
  }
377
294
  }
378
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwcm94eS5wb3J0cHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHByb3h5LnBvcnRwcm94eS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGNBQWMsQ0FBQztBQXFCeEM7OztHQUdHO0FBQ0gsU0FBUyxVQUFVLENBQUMsTUFBYztJQUNoQyxJQUFJLE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDZixrREFBa0Q7SUFDbEQsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3RCLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFFRCxvQkFBb0I7SUFDcEIsTUFBTSxVQUFVLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN2QyxJQUFJLFVBQVUsS0FBSyxFQUFFLEVBQUUsQ0FBQyxDQUFDLGlCQUFpQjtRQUN4QyxPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBQ0QscUJBQXFCO0lBQ3JCLE1BQU0sWUFBWSxHQUFHLE1BQU0sQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDNUMsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUMsR0FBRyxZQUFZLEVBQUUsQ0FBQztRQUNyQyxrRkFBa0Y7UUFDbEYsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUVELE1BQU0sR0FBRyxDQUFDLENBQUM7SUFDWCxzREFBc0Q7SUFDdEQsTUFBTSxhQUFhLEdBQUcsTUFBTSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUMvQyxJQUFJLGFBQWEsS0FBSyxDQUFDLEVBQUUsQ0FBQztRQUN4QixPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBQ0QsdURBQXVEO0lBQ3ZELE1BQU0sSUFBSSxDQUFDLENBQUM7SUFFWixzREFBc0Q7SUFDdEQsTUFBTSxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7SUFFakIsYUFBYTtJ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
295
+ //# 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.2",
3
+ "version": "3.10.4",
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.2',
6
+ version: '3.10.4',
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
  }
@@ -112,18 +79,21 @@ function extractSNI(buffer: Buffer): string | undefined {
112
79
  return undefined;
113
80
  }
114
81
 
82
+ interface IConnectionRecord {
83
+ incoming: plugins.net.Socket;
84
+ outgoing: plugins.net.Socket | null;
85
+ incomingStartTime: number;
86
+ outgoingStartTime?: number;
87
+ connectionClosed: boolean;
88
+ }
89
+
115
90
  export class PortProxy {
116
91
  netServer: plugins.net.Server;
117
92
  settings: IProxySettings;
118
- // Track active incoming connections
119
- private activeConnections: Set<plugins.net.Socket> = new Set();
120
- // Record start times for incoming connections
121
- private incomingConnectionTimes: Map<plugins.net.Socket, number> = new Map();
122
- // Record start times for outgoing connections
123
- private outgoingConnectionTimes: Map<plugins.net.Socket, number> = new Map();
93
+ // Unified record tracking each connection pair.
94
+ private connectionRecords: Set<IConnectionRecord> = new Set();
124
95
  private connectionLogger: NodeJS.Timeout | null = null;
125
96
 
126
- // Overall termination statistics
127
97
  private terminationStats: {
128
98
  incoming: Record<string, number>;
129
99
  outgoing: Record<string, number>;
@@ -135,102 +105,89 @@ export class PortProxy {
135
105
  constructor(settings: IProxySettings) {
136
106
  this.settings = {
137
107
  ...settings,
138
- toHost: settings.toHost || 'localhost'
108
+ toHost: settings.toHost || 'localhost',
139
109
  };
140
110
  }
141
111
 
142
- // Helper to update termination stats.
143
112
  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
- }
113
+ this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
149
114
  }
150
115
 
151
116
  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
- }
117
+ // Helper to forcefully destroy sockets.
118
+ const cleanUpSockets = (socketA: plugins.net.Socket, socketB?: plugins.net.Socket) => {
119
+ if (!socketA.destroyed) socketA.destroy();
120
+ if (socketB && !socketB.destroyed) socketB.destroy();
160
121
  };
161
122
 
123
+ // Normalize an IP to include both IPv4 and IPv6 representations.
162
124
  const normalizeIP = (ip: string): string[] => {
163
- // Handle IPv4-mapped IPv6 addresses
164
125
  if (ip.startsWith('::ffff:')) {
165
- const ipv4 = ip.slice(7); // Remove '::ffff:' prefix
126
+ const ipv4 = ip.slice(7);
166
127
  return [ip, ipv4];
167
128
  }
168
- // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
169
129
  if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
170
130
  return [ip, `::ffff:${ip}`];
171
131
  }
172
132
  return [ip];
173
133
  };
174
134
 
175
- const isAllowed = (value: string, patterns: string[]): boolean => {
176
- // Expand patterns to include both IPv4 and IPv6 variants
135
+ // Check if a given IP matches any of the glob patterns.
136
+ const isAllowed = (ip: string, patterns: string[]): boolean => {
137
+ const normalizedIPVariants = normalizeIP(ip);
177
138
  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))
139
+ return normalizedIPVariants.some(ipVariant =>
140
+ expandedPatterns.some(pattern => plugins.minimatch(ipVariant, pattern))
181
141
  );
182
142
  };
183
143
 
184
- const findMatchingDomain = (serverName: string): IDomainConfig | undefined => {
185
- return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
186
- };
144
+ // Find a matching domain config based on the SNI.
145
+ const findMatchingDomain = (serverName: string): IDomainConfig | undefined =>
146
+ this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
187
147
 
188
- // Create a plain net server for TLS passthrough.
189
148
  this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
190
149
  const remoteIP = socket.remoteAddress || '';
150
+ const connectionRecord: IConnectionRecord = {
151
+ incoming: socket,
152
+ outgoing: null,
153
+ incomingStartTime: Date.now(),
154
+ connectionClosed: false,
155
+ };
156
+ this.connectionRecords.add(connectionRecord);
157
+ console.log(`New connection from ${remoteIP}. Active connections: ${this.connectionRecords.size}`);
191
158
 
192
- // Record start time for the incoming connection.
193
- this.activeConnections.add(socket);
194
- this.incomingConnectionTimes.set(socket, Date.now());
195
- console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`);
196
-
197
- // Flag to detect if we've received the first data chunk.
198
159
  let initialDataReceived = false;
160
+ let incomingTerminationReason: string | null = null;
161
+ let outgoingTerminationReason: string | null = null;
199
162
 
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}`);
163
+ // Ensure cleanup happens only once for the entire connection record.
164
+ const cleanupOnce = () => {
165
+ if (!connectionRecord.connectionClosed) {
166
+ connectionRecord.connectionClosed = true;
167
+ cleanUpSockets(connectionRecord.incoming, connectionRecord.outgoing || undefined);
168
+ this.connectionRecords.delete(connectionRecord);
169
+ console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.connectionRecords.size}`);
210
170
  }
211
- });
171
+ };
212
172
 
213
- // Ensure cleanup happens only once.
214
- let connectionClosed = false;
215
- const cleanupOnce = () => {
216
- if (!connectionClosed) {
217
- connectionClosed = true;
218
- cleanUpSockets(socket, to || undefined);
219
- this.incomingConnectionTimes.delete(socket);
220
- if (to) {
221
- this.outgoingConnectionTimes.delete(to);
222
- }
223
- if (this.activeConnections.has(socket)) {
224
- this.activeConnections.delete(socket);
225
- console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.activeConnections.size}`);
226
- }
173
+ // Helper to reject an incoming connection.
174
+ const rejectIncomingConnection = (reason: string, logMessage: string) => {
175
+ console.log(logMessage);
176
+ socket.end();
177
+ if (incomingTerminationReason === null) {
178
+ incomingTerminationReason = reason;
179
+ this.incrementTerminationStat('incoming', reason);
227
180
  }
181
+ cleanupOnce();
228
182
  };
229
183
 
230
- // Outgoing connection placeholder.
231
- let to: plugins.net.Socket | null = null;
184
+ socket.on('error', (err: Error) => {
185
+ const errorMessage = initialDataReceived
186
+ ? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
187
+ : `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
188
+ console.log(errorMessage);
189
+ });
232
190
 
233
- // Handle errors by recording termination reason and cleaning up.
234
191
  const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
235
192
  const code = (err as any).code;
236
193
  let reason = 'error';
@@ -240,73 +197,47 @@ export class PortProxy {
240
197
  } else {
241
198
  console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
242
199
  }
243
- if (side === 'incoming' && incomingTermReason === null) {
244
- incomingTermReason = reason;
200
+ if (side === 'incoming' && incomingTerminationReason === null) {
201
+ incomingTerminationReason = reason;
245
202
  this.incrementTerminationStat('incoming', reason);
246
- } else if (side === 'outgoing' && outgoingTermReason === null) {
247
- outgoingTermReason = reason;
203
+ } else if (side === 'outgoing' && outgoingTerminationReason === null) {
204
+ outgoingTerminationReason = reason;
248
205
  this.incrementTerminationStat('outgoing', reason);
249
206
  }
250
207
  cleanupOnce();
251
208
  };
252
209
 
253
- // Handle close events. If no termination reason was recorded, mark as "normal".
254
210
  const handleClose = (side: 'incoming' | 'outgoing') => () => {
255
211
  console.log(`Connection closed on ${side} side from ${remoteIP}`);
256
- if (side === 'incoming' && incomingTermReason === null) {
257
- incomingTermReason = 'normal';
212
+ if (side === 'incoming' && incomingTerminationReason === null) {
213
+ incomingTerminationReason = 'normal';
258
214
  this.incrementTerminationStat('incoming', 'normal');
259
- } else if (side === 'outgoing' && outgoingTermReason === null) {
260
- outgoingTermReason = 'normal';
215
+ } else if (side === 'outgoing' && outgoingTerminationReason === null) {
216
+ outgoingTerminationReason = 'normal';
261
217
  this.incrementTerminationStat('outgoing', 'normal');
262
218
  }
263
219
  cleanupOnce();
264
220
  };
265
221
 
266
- // Setup connection, optionally accepting the initial data chunk.
267
222
  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) {
223
+ const defaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
224
+
225
+ if (!defaultAllowed && serverName) {
271
226
  const domainConfig = findMatchingDomain(serverName);
272
227
  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;
228
+ return rejectIncomingConnection('rejected', `Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
281
229
  }
282
230
  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;
291
- }
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');
231
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
298
232
  }
299
- cleanupOnce();
300
- return;
301
- } else {
233
+ } else if (!defaultAllowed && !serverName) {
234
+ return rejectIncomingConnection('rejected', `Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
235
+ } else if (defaultAllowed && !serverName) {
302
236
  console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
303
237
  }
304
238
 
305
- // Determine target host.
306
239
  const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
307
240
  const targetHost = domainConfig?.targetIP || this.settings.toHost!;
308
-
309
- // Create connection options.
310
241
  const connectionOptions: plugins.net.NetConnectOpts = {
311
242
  host: targetHost,
312
243
  port: this.settings.toPort,
@@ -315,49 +246,47 @@ export class PortProxy {
315
246
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
316
247
  }
317
248
 
318
- // Establish outgoing connection.
319
- to = plugins.net.connect(connectionOptions);
320
- if (to) {
321
- this.outgoingConnectionTimes.set(to, Date.now());
322
- }
323
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
324
-
325
- // Push back the initial chunk if provided.
249
+ const targetSocket = plugins.net.connect(connectionOptions);
250
+ connectionRecord.outgoing = targetSocket;
251
+ connectionRecord.outgoingStartTime = Date.now();
252
+
253
+ console.log(
254
+ `Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}` +
255
+ `${serverName ? ` (SNI: ${serverName})` : ''}`
256
+ );
257
+
326
258
  if (initialChunk) {
327
259
  socket.unshift(initialChunk);
328
260
  }
329
261
  socket.setTimeout(120000);
330
- socket.pipe(to!);
331
- to!.pipe(socket);
262
+ socket.pipe(targetSocket);
263
+ targetSocket.pipe(socket);
332
264
 
333
- // Attach event handlers for both sockets.
334
265
  socket.on('error', handleError('incoming'));
335
- to!.on('error', handleError('outgoing'));
266
+ targetSocket.on('error', handleError('outgoing'));
336
267
  socket.on('close', handleClose('incoming'));
337
- to!.on('close', handleClose('outgoing'));
268
+ targetSocket.on('close', handleClose('outgoing'));
338
269
  socket.on('timeout', () => {
339
270
  console.log(`Timeout on incoming side from ${remoteIP}`);
340
- if (incomingTermReason === null) {
341
- incomingTermReason = 'timeout';
271
+ if (incomingTerminationReason === null) {
272
+ incomingTerminationReason = 'timeout';
342
273
  this.incrementTerminationStat('incoming', 'timeout');
343
274
  }
344
275
  cleanupOnce();
345
276
  });
346
- to!.on('timeout', () => {
277
+ targetSocket.on('timeout', () => {
347
278
  console.log(`Timeout on outgoing side from ${remoteIP}`);
348
- if (outgoingTermReason === null) {
349
- outgoingTermReason = 'timeout';
279
+ if (outgoingTerminationReason === null) {
280
+ outgoingTerminationReason = 'timeout';
350
281
  this.incrementTerminationStat('outgoing', 'timeout');
351
282
  }
352
283
  cleanupOnce();
353
284
  });
354
285
  socket.on('end', handleClose('incoming'));
355
- to!.on('end', handleClose('outgoing'));
286
+ targetSocket.on('end', handleClose('outgoing'));
356
287
  };
357
288
 
358
- // For SNI-enabled connections, set an initial data timeout before waiting for data.
359
289
  if (this.settings.sniEnabled) {
360
- // Set an initial timeout for receiving data (e.g., 5 seconds)
361
290
  socket.setTimeout(5000, () => {
362
291
  console.log(`Initial data timeout for ${remoteIP}`);
363
292
  socket.end();
@@ -365,7 +294,6 @@ export class PortProxy {
365
294
  });
366
295
 
367
296
  socket.once('data', (chunk: Buffer) => {
368
- // Clear the initial timeout since data has been received
369
297
  socket.setTimeout(0);
370
298
  initialDataReceived = true;
371
299
  const serverName = extractSNI(chunk) || '';
@@ -373,47 +301,40 @@ export class PortProxy {
373
301
  setupConnection(serverName, chunk);
374
302
  });
375
303
  } else {
376
- // For non-SNI connections, simply check defaultAllowedIPs.
377
304
  initialDataReceived = true;
378
305
  if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
379
- console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
380
- socket.end();
381
- if (incomingTermReason === null) {
382
- incomingTermReason = 'rejected';
383
- this.incrementTerminationStat('incoming', 'rejected');
384
- }
385
- cleanupOnce();
386
- return;
306
+ return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
387
307
  }
388
308
  setupConnection('');
389
309
  }
390
310
  })
391
- .on('error', (err: Error) => {
392
- console.log(`Server Error: ${err.message}`);
393
- })
394
- .listen(this.settings.fromPort, () => {
395
- console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
396
- });
311
+ .on('error', (err: Error) => {
312
+ console.log(`Server Error: ${err.message}`);
313
+ })
314
+ .listen(this.settings.fromPort, () => {
315
+ console.log(
316
+ `PortProxy -> OK: Now listening on port ${this.settings.fromPort}` +
317
+ `${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`
318
+ );
319
+ });
397
320
 
398
- // Log active connection count, longest running connection durations,
399
- // and termination statistics every 10 seconds.
321
+ // Every 10 seconds log active connection count and longest running durations.
400
322
  this.connectionLogger = setInterval(() => {
401
323
  const now = Date.now();
402
324
  let maxIncoming = 0;
403
- for (const startTime of this.incomingConnectionTimes.values()) {
404
- const duration = now - startTime;
405
- if (duration > maxIncoming) {
406
- maxIncoming = duration;
407
- }
408
- }
409
325
  let maxOutgoing = 0;
410
- for (const startTime of this.outgoingConnectionTimes.values()) {
411
- const duration = now - startTime;
412
- if (duration > maxOutgoing) {
413
- maxOutgoing = duration;
326
+ for (const record of this.connectionRecords) {
327
+ maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
328
+ if (record.outgoingStartTime) {
329
+ maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
414
330
  }
415
331
  }
416
- 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)}`);
332
+ console.log(
333
+ `(Interval Log) Active connections: ${this.connectionRecords.size}. ` +
334
+ `Longest running incoming: ${plugins.prettyMs(maxIncoming)}, outgoing: ${plugins.prettyMs(maxOutgoing)}. ` +
335
+ `Termination stats (incoming): ${JSON.stringify(this.terminationStats.incoming)}, ` +
336
+ `(outgoing): ${JSON.stringify(this.terminationStats.outgoing)}`
337
+ );
417
338
  }, 10000);
418
339
  }
419
340