@push.rocks/smartproxy 3.7.3 → 3.8.0

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.7.3',
6
+ version: '3.8.0',
7
7
  description: 'a proxy for handling high workloads of proxying'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLGlEQUFpRDtDQUMvRCxDQUFBIn0=
@@ -16,6 +16,8 @@ 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 connectionLogger;
19
21
  constructor(settings: IProxySettings);
20
22
  start(): Promise<void>;
21
23
  stop(): Promise<void>;
@@ -85,6 +85,9 @@ function extractSNI(buffer) {
85
85
  }
86
86
  export class PortProxy {
87
87
  constructor(settings) {
88
+ // Track active incoming connections
89
+ this.activeConnections = new Set();
90
+ this.connectionLogger = null;
88
91
  this.settings = {
89
92
  ...settings,
90
93
  toHost: settings.toHost || 'localhost'
@@ -122,77 +125,68 @@ export class PortProxy {
122
125
  const findMatchingDomain = (serverName) => {
123
126
  return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
124
127
  };
125
- // Always create a plain net server for TLS passthrough.
128
+ // Create a plain net server for TLS passthrough.
126
129
  this.netServer = plugins.net.createServer((socket) => {
127
130
  const remoteIP = socket.remoteAddress || '';
128
- // If SNI is enabled, we peek at the first chunk to extract the SNI.
129
- if (this.settings.sniEnabled) {
130
- socket.once('data', (chunk) => {
131
- // Try to extract the server name from the ClientHello.
132
- const serverName = extractSNI(chunk) || '';
133
- console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
134
- // Check if the IP is allowed by default.
135
- const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
136
- if (!isDefaultAllowed && serverName) {
137
- const domainConfig = findMatchingDomain(serverName);
138
- if (!domainConfig) {
139
- console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`);
140
- socket.end();
141
- return;
142
- }
143
- if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
144
- console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
145
- socket.end();
146
- return;
147
- }
131
+ // Track the new incoming connection.
132
+ this.activeConnections.add(socket);
133
+ console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`);
134
+ // Flag to ensure cleanup happens only once.
135
+ let connectionClosed = false;
136
+ const cleanupOnce = () => {
137
+ if (!connectionClosed) {
138
+ connectionClosed = true;
139
+ cleanUpSockets(socket, to);
140
+ if (this.activeConnections.has(socket)) {
141
+ this.activeConnections.delete(socket);
142
+ console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.activeConnections.size}`);
148
143
  }
149
- else if (!isDefaultAllowed && !serverName) {
150
- console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
144
+ }
145
+ };
146
+ let to;
147
+ const handleError = (side) => (err) => {
148
+ const code = err.code;
149
+ if (code === 'ECONNRESET') {
150
+ console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
151
+ }
152
+ else {
153
+ console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
154
+ }
155
+ cleanupOnce();
156
+ };
157
+ const handleClose = (side) => () => {
158
+ console.log(`Connection closed on ${side} side from ${remoteIP}`);
159
+ cleanupOnce();
160
+ };
161
+ // Setup connection, optionally accepting the initial data chunk.
162
+ const setupConnection = (serverName, initialChunk) => {
163
+ // Check if the IP is allowed by default.
164
+ const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
165
+ if (!isDefaultAllowed && serverName) {
166
+ const domainConfig = findMatchingDomain(serverName);
167
+ if (!domainConfig) {
168
+ console.log(`Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
151
169
  socket.end();
152
170
  return;
153
171
  }
154
- else {
155
- console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
156
- }
157
- // Determine target host.
158
- const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
159
- const targetHost = domainConfig?.targetIP || this.settings.toHost;
160
- // Create connection options.
161
- const connectionOptions = {
162
- host: targetHost,
163
- port: this.settings.toPort,
164
- };
165
- if (this.settings.preserveSourceIP) {
166
- connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
172
+ if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
173
+ console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
174
+ socket.end();
175
+ return;
167
176
  }
168
- const to = plugins.net.connect(connectionOptions);
169
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
170
- // Unshift the data chunk back so that the TLS handshake can complete at the backend.
171
- socket.unshift(chunk);
172
- socket.setTimeout(120000);
173
- socket.pipe(to);
174
- to.pipe(socket);
175
- const errorHandler = () => {
176
- cleanUpSockets(socket, to);
177
- };
178
- socket.on('error', errorHandler);
179
- to.on('error', errorHandler);
180
- socket.on('close', errorHandler);
181
- to.on('close', errorHandler);
182
- socket.on('timeout', errorHandler);
183
- to.on('timeout', errorHandler);
184
- socket.on('end', errorHandler);
185
- to.on('end', errorHandler);
186
- });
187
- }
188
- else {
189
- // If SNI is not enabled, use defaultAllowedIPs check.
190
- if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
191
- console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
177
+ }
178
+ else if (!isDefaultAllowed && !serverName) {
179
+ console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
192
180
  socket.end();
193
181
  return;
194
182
  }
195
- const targetHost = this.settings.toHost;
183
+ else {
184
+ console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
185
+ }
186
+ // Determine target host.
187
+ const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
188
+ const targetHost = domainConfig?.targetIP || this.settings.toHost;
189
+ // Create connection options.
196
190
  const connectionOptions = {
197
191
  host: targetHost,
198
192
  port: this.settings.toPort,
@@ -200,22 +194,43 @@ export class PortProxy {
200
194
  if (this.settings.preserveSourceIP) {
201
195
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
202
196
  }
203
- const to = plugins.net.connect(connectionOptions);
204
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}`);
197
+ // Establish outgoing connection.
198
+ to = plugins.net.connect(connectionOptions);
199
+ console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
200
+ // Push back the initial chunk if provided.
201
+ if (initialChunk) {
202
+ socket.unshift(initialChunk);
203
+ }
205
204
  socket.setTimeout(120000);
206
205
  socket.pipe(to);
207
206
  to.pipe(socket);
208
- const errorHandler = () => {
209
- cleanUpSockets(socket, to);
210
- };
211
- socket.on('error', errorHandler);
212
- to.on('error', errorHandler);
213
- socket.on('close', errorHandler);
214
- to.on('close', errorHandler);
215
- socket.on('timeout', errorHandler);
216
- to.on('timeout', errorHandler);
217
- socket.on('end', errorHandler);
218
- to.on('end', errorHandler);
207
+ // Attach error and close handlers for both sockets.
208
+ socket.on('error', handleError('incoming'));
209
+ to.on('error', handleError('outgoing'));
210
+ socket.on('close', handleClose('incoming'));
211
+ to.on('close', handleClose('outgoing'));
212
+ socket.on('timeout', handleError('incoming'));
213
+ to.on('timeout', handleError('outgoing'));
214
+ socket.on('end', handleClose('incoming'));
215
+ to.on('end', handleClose('outgoing'));
216
+ };
217
+ // For SNI-enabled connections, peek at the first chunk.
218
+ if (this.settings.sniEnabled) {
219
+ socket.once('data', (chunk) => {
220
+ // Try to extract the server name from the ClientHello.
221
+ const serverName = extractSNI(chunk) || '';
222
+ console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
223
+ setupConnection(serverName, chunk);
224
+ });
225
+ }
226
+ else {
227
+ // For non-SNI connections, simply check defaultAllowedIPs.
228
+ if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
229
+ console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
230
+ socket.end();
231
+ return;
232
+ }
233
+ setupConnection('');
219
234
  }
220
235
  })
221
236
  .on('error', (err) => {
@@ -224,13 +239,21 @@ export class PortProxy {
224
239
  .listen(this.settings.fromPort, () => {
225
240
  console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
226
241
  });
242
+ // Log active connection count every 10 seconds.
243
+ this.connectionLogger = setInterval(() => {
244
+ console.log(`(Interval Log) Active connections: ${this.activeConnections.size}`);
245
+ }, 10000);
227
246
  }
228
247
  async stop() {
229
248
  const done = plugins.smartpromise.defer();
230
249
  this.netServer.close(() => {
231
250
  done.resolve();
232
251
  });
252
+ if (this.connectionLogger) {
253
+ clearInterval(this.connectionLogger);
254
+ this.connectionLogger = null;
255
+ }
233
256
  await done.promise;
234
257
  }
235
258
  }
236
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwcm94eS5wb3J0cHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHByb3h5LnBvcnRwcm94eS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLHlCQUF5QixDQUFDO0FBcUJuRDs7O0dBR0c7QUFDSCxTQUFTLFVBQVUsQ0FBQyxNQUFjO0lBQ2hDLElBQUksTUFBTSxHQUFHLENBQUMsQ0FBQztJQUNmLGtEQUFrRDtJQUNsRCxJQUFJLE1BQU0sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDdEIsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUVELG9CQUFvQjtJQUNwQixNQUFNLFVBQVUsR0FBRyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3ZDLElBQUksVUFBVSxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsaUJBQWlCO1FBQ3hDLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFDRCxxQkFBcUI7SUFDckIsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUM1QyxJQUFJLE1BQU0sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxHQUFHLFlBQVksRUFBRSxDQUFDO1FBQ3JDLGtGQUFrRjtRQUNsRixPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBRUQsTUFBTSxHQUFHLENBQUMsQ0FBQztJQUNYLHNEQUFzRDtJQUN0RCxNQUFNLGFBQWEsR0FBRyxNQUFNLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQy9DLElBQUksYUFBYSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3hCLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFDRCx1REFBdUQ7SUFDdkQsTUFBTSxJQUFJLENBQUMsQ0FBQztJQUVaLHNEQUFzRDtJQUN0RCxNQUFNLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUVqQixhQUFh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
259
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwcm94eS5wb3J0cHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHByb3h5LnBvcnRwcm94eS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLHlCQUF5QixDQUFDO0FBcUJuRDs7O0dBR0c7QUFDSCxTQUFTLFVBQVUsQ0FBQyxNQUFjO0lBQ2hDLElBQUksTUFBTSxHQUFHLENBQUMsQ0FBQztJQUNmLGtEQUFrRDtJQUNsRCxJQUFJLE1BQU0sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDdEIsT0FBTyxTQUFTLENBQUM7SUFDbkIsQ0FBQztJQUVELG9CQUFvQjtJQUNwQixNQUFNLFVBQVUsR0FBRyxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQ3ZDLElBQUksVUFBVSxLQUFLLEVBQUUsRUFBRSxDQUFDLENBQUMsaUJBQWlCO1FBQ3hDLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFDRCxxQkFBcUI7SUFDckIsTUFBTSxZQUFZLEdBQUcsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUM1QyxJQUFJLE1BQU0sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxHQUFHLFlBQVksRUFBRSxDQUFDO1FBQ3JDLGtGQUFrRjtRQUNsRixPQUFPLFNBQVMsQ0FBQztJQUNuQixDQUFDO0lBRUQsTUFBTSxHQUFHLENBQUMsQ0FBQztJQUNYLHNEQUFzRDtJQUN0RCxNQUFNLGFBQWEsR0FBRyxNQUFNLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQy9DLElBQUksYUFBYSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQ3hCLE9BQU8sU0FBUyxDQUFDO0lBQ25CLENBQUM7SUFDRCx1REFBdUQ7SUFDdkQsTUFBTSxJQUFJLENBQUMsQ0FBQztJQUVaLHNEQUFzRDtJQUN0RCxNQUFNLElBQUksQ0FBQyxHQUFHLEVBQUUsQ0FBQztJQUVqQixhQUFh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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartproxy",
3
- "version": "3.7.3",
3
+ "version": "3.8.0",
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.7.3',
6
+ version: '3.8.0',
7
7
  description: 'a proxy for handling high workloads of proxying'
8
8
  }
@@ -115,6 +115,9 @@ function extractSNI(buffer: Buffer): string | undefined {
115
115
  export class PortProxy {
116
116
  netServer: plugins.net.Server;
117
117
  settings: IProxySettings;
118
+ // Track active incoming connections
119
+ private activeConnections: Set<plugins.net.Socket> = new Set();
120
+ private connectionLogger: NodeJS.Timeout | null = null;
118
121
 
119
122
  constructor(settings: IProxySettings) {
120
123
  this.settings = {
@@ -161,81 +164,73 @@ export class PortProxy {
161
164
  return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
162
165
  };
163
166
 
164
- // Always create a plain net server for TLS passthrough.
167
+ // Create a plain net server for TLS passthrough.
165
168
  this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
166
169
  const remoteIP = socket.remoteAddress || '';
167
170
 
168
- // If SNI is enabled, we peek at the first chunk to extract the SNI.
169
- if (this.settings.sniEnabled) {
170
- socket.once('data', (chunk: Buffer) => {
171
- // Try to extract the server name from the ClientHello.
172
- const serverName = extractSNI(chunk) || '';
173
- console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
171
+ // Track the new incoming connection.
172
+ this.activeConnections.add(socket);
173
+ console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`);
174
+
175
+ // Flag to ensure cleanup happens only once.
176
+ let connectionClosed = false;
177
+ const cleanupOnce = () => {
178
+ if (!connectionClosed) {
179
+ connectionClosed = true;
180
+ cleanUpSockets(socket, to);
181
+ if (this.activeConnections.has(socket)) {
182
+ this.activeConnections.delete(socket);
183
+ console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.activeConnections.size}`);
184
+ }
185
+ }
186
+ };
187
+
188
+ let to: plugins.net.Socket;
189
+
190
+ const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
191
+ const code = (err as any).code;
192
+ if (code === 'ECONNRESET') {
193
+ console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
194
+ } else {
195
+ console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
196
+ }
197
+ cleanupOnce();
198
+ };
199
+
200
+ const handleClose = (side: 'incoming' | 'outgoing') => () => {
201
+ console.log(`Connection closed on ${side} side from ${remoteIP}`);
202
+ cleanupOnce();
203
+ };
174
204
 
175
- // Check if the IP is allowed by default.
176
- const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
177
- if (!isDefaultAllowed && serverName) {
178
- const domainConfig = findMatchingDomain(serverName);
179
- if (!domainConfig) {
180
- console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`);
181
- socket.end();
182
- return;
183
- }
184
- if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
185
- console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
186
- socket.end();
187
- return;
188
- }
189
- } else if (!isDefaultAllowed && !serverName) {
190
- console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
205
+ // Setup connection, optionally accepting the initial data chunk.
206
+ const setupConnection = (serverName: string, initialChunk?: Buffer) => {
207
+ // Check if the IP is allowed by default.
208
+ const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
209
+ if (!isDefaultAllowed && serverName) {
210
+ const domainConfig = findMatchingDomain(serverName);
211
+ if (!domainConfig) {
212
+ console.log(`Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
191
213
  socket.end();
192
214
  return;
193
- } else {
194
- console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
195
215
  }
196
-
197
- // Determine target host.
198
- const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
199
- const targetHost = domainConfig?.targetIP || this.settings.toHost!;
200
-
201
- // Create connection options.
202
- const connectionOptions: plugins.net.NetConnectOpts = {
203
- host: targetHost,
204
- port: this.settings.toPort,
205
- };
206
- if (this.settings.preserveSourceIP) {
207
- connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
216
+ if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
217
+ console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
218
+ socket.end();
219
+ return;
208
220
  }
209
-
210
- const to = plugins.net.connect(connectionOptions);
211
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
212
-
213
- // Unshift the data chunk back so that the TLS handshake can complete at the backend.
214
- socket.unshift(chunk);
215
- socket.setTimeout(120000);
216
- socket.pipe(to);
217
- to.pipe(socket);
218
-
219
- const errorHandler = () => {
220
- cleanUpSockets(socket, to);
221
- };
222
- socket.on('error', errorHandler);
223
- to.on('error', errorHandler);
224
- socket.on('close', errorHandler);
225
- to.on('close', errorHandler);
226
- socket.on('timeout', errorHandler);
227
- to.on('timeout', errorHandler);
228
- socket.on('end', errorHandler);
229
- to.on('end', errorHandler);
230
- });
231
- } else {
232
- // If SNI is not enabled, use defaultAllowedIPs check.
233
- if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
234
- console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
221
+ } else if (!isDefaultAllowed && !serverName) {
222
+ console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
235
223
  socket.end();
236
224
  return;
225
+ } else {
226
+ console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
237
227
  }
238
- const targetHost = this.settings.toHost!;
228
+
229
+ // Determine target host.
230
+ const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
231
+ const targetHost = domainConfig?.targetIP || this.settings.toHost!;
232
+
233
+ // Create connection options.
239
234
  const connectionOptions: plugins.net.NetConnectOpts = {
240
235
  host: targetHost,
241
236
  port: this.settings.toPort,
@@ -243,22 +238,46 @@ export class PortProxy {
243
238
  if (this.settings.preserveSourceIP) {
244
239
  connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
245
240
  }
246
- const to = plugins.net.connect(connectionOptions);
247
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}`);
241
+
242
+ // Establish outgoing connection.
243
+ to = plugins.net.connect(connectionOptions);
244
+ console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
245
+
246
+ // Push back the initial chunk if provided.
247
+ if (initialChunk) {
248
+ socket.unshift(initialChunk);
249
+ }
248
250
  socket.setTimeout(120000);
249
251
  socket.pipe(to);
250
252
  to.pipe(socket);
251
- const errorHandler = () => {
252
- cleanUpSockets(socket, to);
253
- };
254
- socket.on('error', errorHandler);
255
- to.on('error', errorHandler);
256
- socket.on('close', errorHandler);
257
- to.on('close', errorHandler);
258
- socket.on('timeout', errorHandler);
259
- to.on('timeout', errorHandler);
260
- socket.on('end', errorHandler);
261
- to.on('end', errorHandler);
253
+
254
+ // Attach error and close handlers for both sockets.
255
+ socket.on('error', handleError('incoming'));
256
+ to.on('error', handleError('outgoing'));
257
+ socket.on('close', handleClose('incoming'));
258
+ to.on('close', handleClose('outgoing'));
259
+ socket.on('timeout', handleError('incoming'));
260
+ to.on('timeout', handleError('outgoing'));
261
+ socket.on('end', handleClose('incoming'));
262
+ to.on('end', handleClose('outgoing'));
263
+ };
264
+
265
+ // For SNI-enabled connections, peek at the first chunk.
266
+ if (this.settings.sniEnabled) {
267
+ socket.once('data', (chunk: Buffer) => {
268
+ // Try to extract the server name from the ClientHello.
269
+ const serverName = extractSNI(chunk) || '';
270
+ console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
271
+ setupConnection(serverName, chunk);
272
+ });
273
+ } else {
274
+ // For non-SNI connections, simply check defaultAllowedIPs.
275
+ if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
276
+ console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
277
+ socket.end();
278
+ return;
279
+ }
280
+ setupConnection('');
262
281
  }
263
282
  })
264
283
  .on('error', (err: Error) => {
@@ -267,6 +286,11 @@ export class PortProxy {
267
286
  .listen(this.settings.fromPort, () => {
268
287
  console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
269
288
  });
289
+
290
+ // Log active connection count every 10 seconds.
291
+ this.connectionLogger = setInterval(() => {
292
+ console.log(`(Interval Log) Active connections: ${this.activeConnections.size}`);
293
+ }, 10000);
270
294
  }
271
295
 
272
296
  public async stop() {
@@ -274,6 +298,10 @@ export class PortProxy {
274
298
  this.netServer.close(() => {
275
299
  done.resolve();
276
300
  });
301
+ if (this.connectionLogger) {
302
+ clearInterval(this.connectionLogger);
303
+ this.connectionLogger = null;
304
+ }
277
305
  await done.promise;
278
306
  }
279
307
  }