@push.rocks/smartproxy 3.7.2 → 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.2',
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=
@@ -1,22 +1,24 @@
1
1
  import * as plugins from './smartproxy.plugins.js';
2
- export interface DomainConfig {
2
+ export interface IDomainConfig {
3
3
  domain: string;
4
4
  allowedIPs: string[];
5
5
  targetIP?: string;
6
6
  }
7
- export interface ProxySettings extends plugins.tls.TlsOptions {
7
+ export interface IProxySettings extends plugins.tls.TlsOptions {
8
8
  fromPort: number;
9
9
  toPort: number;
10
10
  toHost?: string;
11
- domains: DomainConfig[];
11
+ domains: IDomainConfig[];
12
12
  sniEnabled?: boolean;
13
13
  defaultAllowedIPs?: string[];
14
14
  preserveSourceIP?: boolean;
15
15
  }
16
16
  export declare class PortProxy {
17
- netServer: plugins.net.Server | plugins.tls.Server;
18
- settings: ProxySettings;
19
- constructor(settings: ProxySettings);
17
+ netServer: plugins.net.Server;
18
+ settings: IProxySettings;
19
+ private activeConnections;
20
+ private connectionLogger;
21
+ constructor(settings: IProxySettings);
20
22
  start(): Promise<void>;
21
23
  stop(): Promise<void>;
22
24
  }
@@ -1,6 +1,93 @@
1
1
  import * as plugins from './smartproxy.plugins.js';
2
+ /**
3
+ * Extract SNI (Server Name Indication) from a TLS ClientHello packet.
4
+ * Returns the server name if found, or undefined.
5
+ */
6
+ function extractSNI(buffer) {
7
+ let offset = 0;
8
+ // We need at least 5 bytes for the record header.
9
+ if (buffer.length < 5) {
10
+ return undefined;
11
+ }
12
+ // TLS record header
13
+ const recordType = buffer.readUInt8(0);
14
+ if (recordType !== 22) { // 22 = handshake
15
+ return undefined;
16
+ }
17
+ // Read record length
18
+ 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.
21
+ return undefined;
22
+ }
23
+ offset = 5;
24
+ // Handshake message type should be 1 for ClientHello.
25
+ 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
34
+ const sessionIDLength = buffer.readUInt8(offset);
35
+ offset += 1 + sessionIDLength;
36
+ // Cipher suites
37
+ const cipherSuitesLength = buffer.readUInt16BE(offset);
38
+ offset += 2 + cipherSuitesLength;
39
+ // Compression methods
40
+ const compressionMethodsLength = buffer.readUInt8(offset);
41
+ offset += 1 + compressionMethodsLength;
42
+ // Extensions length
43
+ if (offset + 2 > buffer.length) {
44
+ return undefined;
45
+ }
46
+ const extensionsLength = buffer.readUInt16BE(offset);
47
+ offset += 2;
48
+ const extensionsEnd = offset + extensionsLength;
49
+ // Iterate over extensions
50
+ while (offset + 4 <= extensionsEnd) {
51
+ const extensionType = buffer.readUInt16BE(offset);
52
+ const extensionLength = buffer.readUInt16BE(offset + 2);
53
+ 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) {
58
+ return undefined;
59
+ }
60
+ const sniListLength = buffer.readUInt16BE(offset);
61
+ offset += 2;
62
+ const sniListEnd = offset + sniListLength;
63
+ // Loop through the list; typically there is one entry.
64
+ while (offset + 3 < sniListEnd) {
65
+ const nameType = buffer.readUInt8(offset);
66
+ offset++;
67
+ const nameLen = buffer.readUInt16BE(offset);
68
+ offset += 2;
69
+ if (nameType === 0) { // host_name
70
+ if (offset + nameLen > buffer.length) {
71
+ return undefined;
72
+ }
73
+ const serverName = buffer.toString('utf8', offset, offset + nameLen);
74
+ return serverName;
75
+ }
76
+ offset += nameLen;
77
+ }
78
+ break;
79
+ }
80
+ else {
81
+ offset += extensionLength;
82
+ }
83
+ }
84
+ return undefined;
85
+ }
2
86
  export class PortProxy {
3
87
  constructor(settings) {
88
+ // Track active incoming connections
89
+ this.activeConnections = new Set();
90
+ this.connectionLogger = null;
4
91
  this.settings = {
5
92
  ...settings,
6
93
  toHost: settings.toHost || 'localhost'
@@ -24,7 +111,7 @@ export class PortProxy {
24
111
  return [ip, ipv4];
25
112
  }
26
113
  // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
27
- if (ip.match(/^\d{1,3}(\.\d{1,3}){3}$/)) {
114
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
28
115
  return [ip, `::ffff:${ip}`];
29
116
  }
30
117
  return [ip];
@@ -38,113 +125,135 @@ export class PortProxy {
38
125
  const findMatchingDomain = (serverName) => {
39
126
  return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
40
127
  };
41
- const server = this.settings.sniEnabled
42
- ? plugins.tls.createServer({
43
- SNICallback: (serverName, cb) => {
44
- console.log(`SNI request for domain: ${serverName}`);
45
- // Create a minimal context just to read SNI, we'll pass through the actual TLS
46
- const ctx = plugins.tls.createSecureContext({
47
- minVersion: 'TLSv1.2',
48
- maxVersion: 'TLSv1.3'
49
- });
50
- cb(null, ctx);
128
+ // Create a plain net server for TLS passthrough.
129
+ this.netServer = plugins.net.createServer((socket) => {
130
+ const remoteIP = socket.remoteAddress || '';
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}`);
143
+ }
51
144
  }
52
- })
53
- : plugins.net.createServer();
54
- const handleConnection = (from) => {
55
- const remoteIP = from.remoteAddress || '';
56
- let serverName = '';
57
- // First check if this IP is in the default allowed list
58
- const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
59
- if (this.settings.sniEnabled && from instanceof plugins.tls.TLSSocket) {
60
- serverName = from.servername || '';
61
- console.log(`TLS Connection from ${remoteIP} for domain: ${serverName}`);
62
- }
63
- // If IP is in defaultAllowedIPs, allow the connection regardless of SNI
64
- if (isDefaultAllowed) {
65
- console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
66
- }
67
- else if (this.settings.sniEnabled && serverName) {
68
- // For SNI connections that aren't in default list, check domain-specific rules
69
- const domainConfig = findMatchingDomain(serverName);
70
- if (!domainConfig) {
71
- console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`);
72
- from.end();
73
- return;
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}`);
74
151
  }
75
- if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
76
- console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
77
- from.end();
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}`);
169
+ socket.end();
170
+ return;
171
+ }
172
+ if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
173
+ console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
174
+ socket.end();
175
+ return;
176
+ }
177
+ }
178
+ else if (!isDefaultAllowed && !serverName) {
179
+ console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
180
+ socket.end();
78
181
  return;
79
182
  }
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.
190
+ const connectionOptions = {
191
+ host: targetHost,
192
+ port: this.settings.toPort,
193
+ };
194
+ if (this.settings.preserveSourceIP) {
195
+ connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
196
+ }
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
+ }
204
+ socket.setTimeout(120000);
205
+ socket.pipe(to);
206
+ to.pipe(socket);
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
+ });
80
225
  }
81
226
  else {
82
- // Non-SNI connection and not in default list
83
- console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
84
- from.end();
85
- return;
86
- }
87
- // Determine target host - use domain-specific targetIP if available
88
- const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
89
- const targetHost = domainConfig?.targetIP || this.settings.toHost;
90
- // Create connection, optionally preserving the client's source IP
91
- const connectionOptions = {
92
- host: targetHost,
93
- port: this.settings.toPort,
94
- };
95
- // Only set localAddress if preserveSourceIP is enabled
96
- if (this.settings.preserveSourceIP) {
97
- connectionOptions.localAddress = remoteIP.replace('::ffff:', ''); // Remove IPv6 mapping if present
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('');
98
234
  }
99
- // If this is a TLS connection, use net.connect to ensure raw passthrough
100
- const to = plugins.net.connect(connectionOptions);
101
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
102
- from.setTimeout(120000);
103
- from.pipe(to);
104
- to.pipe(from);
105
- from.on('error', () => {
106
- cleanUpSockets(from, to);
107
- });
108
- to.on('error', () => {
109
- cleanUpSockets(from, to);
110
- });
111
- from.on('close', () => {
112
- cleanUpSockets(from, to);
113
- });
114
- to.on('close', () => {
115
- cleanUpSockets(from, to);
116
- });
117
- from.on('timeout', () => {
118
- cleanUpSockets(from, to);
119
- });
120
- to.on('timeout', () => {
121
- cleanUpSockets(from, to);
122
- });
123
- from.on('end', () => {
124
- cleanUpSockets(from, to);
125
- });
126
- to.on('end', () => {
127
- cleanUpSockets(from, to);
128
- });
129
- };
130
- this.netServer = server
131
- .on('connection', handleConnection)
132
- .on('secureConnection', handleConnection)
133
- .on('tlsClientError', (err, tlsSocket) => {
134
- console.log(`TLS Client Error: ${err.message}`);
135
235
  })
136
236
  .on('error', (err) => {
137
237
  console.log(`Server Error: ${err.message}`);
138
238
  })
139
- .listen(this.settings.fromPort);
140
- console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI enabled)' : ''}`);
239
+ .listen(this.settings.fromPort, () => {
240
+ console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
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);
141
246
  }
142
247
  async stop() {
143
248
  const done = plugins.smartpromise.defer();
144
249
  this.netServer.close(() => {
145
250
  done.resolve();
146
251
  });
252
+ if (this.connectionLogger) {
253
+ clearInterval(this.connectionLogger);
254
+ this.connectionLogger = null;
255
+ }
147
256
  await done.promise;
148
257
  }
149
258
  }
150
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwcm94eS5wb3J0cHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHByb3h5LnBvcnRwcm94eS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLHlCQUF5QixDQUFDO0FBc0JuRCxNQUFNLE9BQU8sU0FBUztJQUlwQixZQUFZLFFBQXVCO1FBQ2pDLElBQUksQ0FBQyxRQUFRLEdBQUc7WUFDZCxHQUFHLFFBQVE7WUFDWCxNQUFNLEVBQUUsUUFBUSxDQUFDLE1BQU0sSUFBSSxXQUFXO1NBQ3ZDLENBQUM7SUFDSixDQUFDO0lBRU0sS0FBSyxDQUFDLEtBQUs7UUFDaEIsTUFBTSxjQUFjLEdBQUcsQ0FBQyxJQUF3QixFQUFFLEVBQXNCLEVBQUUsRUFBRTtZQUMxRSxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7WUFDWCxFQUFFLENBQUMsR0FBRyxFQUFFLENBQUM7WUFDVCxJQUFJLENBQUMsa0JBQWtCLEVBQUUsQ0FBQztZQUMxQixFQUFFLENBQUMsa0JBQWtCLEVBQUUsQ0FBQztZQUN4QixJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDZCxFQUFFLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDWixJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDZixFQUFFLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDZixDQUFDLENBQUM7UUFDRixNQUFNLFdBQVcsR0FBRyxDQUFDLEVBQVUsRUFBWSxFQUFFO1lBQzNDLG9DQUFvQztZQUNwQyxJQUFJLEVBQUUsQ0FBQyxVQUFVLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztnQkFDN0IsTUFBTSxJQUFJLEdBQUcsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLDBCQUEwQjtnQkFDcEQsT0FBTyxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsQ0FBQztZQUNwQixDQUFDO1lBQ0QsMkRBQTJEO1lBQzNELElBQUksRUFBRSxDQUFD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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.2",
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.2',
6
+ version: '3.8.0',
7
7
  description: 'a proxy for handling high workloads of proxying'
8
8
  }
@@ -1,30 +1,125 @@
1
1
  import * as plugins from './smartproxy.plugins.js';
2
2
 
3
-
4
- export interface DomainConfig {
5
- domain: string; // glob pattern for domain
6
- allowedIPs: string[]; // glob patterns for IPs allowed to access this domain
7
- targetIP?: string; // Optional target IP for this domain
3
+ export interface IDomainConfig {
4
+ domain: string; // glob pattern for domain
5
+ allowedIPs: string[]; // glob patterns for IPs allowed to access this domain
6
+ targetIP?: string; // Optional target IP for this domain
8
7
  }
9
8
 
10
- export interface ProxySettings extends plugins.tls.TlsOptions {
9
+ export interface IProxySettings extends plugins.tls.TlsOptions {
11
10
  // Port configuration
12
11
  fromPort: number;
13
12
  toPort: number;
14
- toHost?: string; // Target host to proxy to, defaults to 'localhost'
13
+ toHost?: string; // Target host to proxy to, defaults to 'localhost'
15
14
 
16
15
  // Domain and security settings
17
- domains: DomainConfig[];
16
+ domains: IDomainConfig[];
18
17
  sniEnabled?: boolean;
19
- defaultAllowedIPs?: string[]; // Optional default IP patterns if no matching domain found
20
- preserveSourceIP?: boolean; // Whether to preserve the client's source IP when proxying
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
20
+ }
21
+
22
+ /**
23
+ * Extract SNI (Server Name Indication) from a TLS ClientHello packet.
24
+ * Returns the server name if found, or undefined.
25
+ */
26
+ function extractSNI(buffer: Buffer): string | undefined {
27
+ let offset = 0;
28
+ // We need at least 5 bytes for the record header.
29
+ if (buffer.length < 5) {
30
+ return undefined;
31
+ }
32
+
33
+ // TLS record header
34
+ const recordType = buffer.readUInt8(0);
35
+ if (recordType !== 22) { // 22 = handshake
36
+ return undefined;
37
+ }
38
+ // Read record length
39
+ 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
+ }
44
+
45
+ offset = 5;
46
+ // Handshake message type should be 1 for ClientHello.
47
+ 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;
53
+
54
+ // Skip client version (2 bytes) and random (32 bytes)
55
+ offset += 2 + 32;
56
+
57
+ // Session ID
58
+ const sessionIDLength = buffer.readUInt8(offset);
59
+ offset += 1 + sessionIDLength;
60
+
61
+ // Cipher suites
62
+ const cipherSuitesLength = buffer.readUInt16BE(offset);
63
+ offset += 2 + cipherSuitesLength;
64
+
65
+ // Compression methods
66
+ const compressionMethodsLength = buffer.readUInt8(offset);
67
+ offset += 1 + compressionMethodsLength;
68
+
69
+ // Extensions length
70
+ if (offset + 2 > buffer.length) {
71
+ return undefined;
72
+ }
73
+ const extensionsLength = buffer.readUInt16BE(offset);
74
+ offset += 2;
75
+ const extensionsEnd = offset + extensionsLength;
76
+
77
+ // Iterate over extensions
78
+ while (offset + 4 <= extensionsEnd) {
79
+ const extensionType = buffer.readUInt16BE(offset);
80
+ const extensionLength = buffer.readUInt16BE(offset + 2);
81
+ 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
+ }
89
+ const sniListLength = buffer.readUInt16BE(offset);
90
+ offset += 2;
91
+ const sniListEnd = offset + sniListLength;
92
+ // Loop through the list; typically there is one entry.
93
+ while (offset + 3 < sniListEnd) {
94
+ const nameType = buffer.readUInt8(offset);
95
+ offset++;
96
+ const nameLen = buffer.readUInt16BE(offset);
97
+ offset += 2;
98
+ 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;
104
+ }
105
+ offset += nameLen;
106
+ }
107
+ break;
108
+ } else {
109
+ offset += extensionLength;
110
+ }
111
+ }
112
+ return undefined;
21
113
  }
22
114
 
23
115
  export class PortProxy {
24
- netServer: plugins.net.Server | plugins.tls.Server;
25
- settings: ProxySettings;
116
+ netServer: plugins.net.Server;
117
+ settings: IProxySettings;
118
+ // Track active incoming connections
119
+ private activeConnections: Set<plugins.net.Socket> = new Set();
120
+ private connectionLogger: NodeJS.Timeout | null = null;
26
121
 
27
- constructor(settings: ProxySettings) {
122
+ constructor(settings: IProxySettings) {
28
123
  this.settings = {
29
124
  ...settings,
30
125
  toHost: settings.toHost || 'localhost'
@@ -42,6 +137,7 @@ export class PortProxy {
42
137
  from.destroy();
43
138
  to.destroy();
44
139
  };
140
+
45
141
  const normalizeIP = (ip: string): string[] => {
46
142
  // Handle IPv4-mapped IPv6 addresses
47
143
  if (ip.startsWith('::ffff:')) {
@@ -49,7 +145,7 @@ export class PortProxy {
49
145
  return [ip, ipv4];
50
146
  }
51
147
  // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
52
- if (ip.match(/^\d{1,3}(\.\d{1,3}){3}$/)) {
148
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
53
149
  return [ip, `::ffff:${ip}`];
54
150
  }
55
151
  return [ip];
@@ -59,122 +155,142 @@ export class PortProxy {
59
155
  // Expand patterns to include both IPv4 and IPv6 variants
60
156
  const expandedPatterns = patterns.flatMap(normalizeIP);
61
157
  // Check if any variant of the IP matches any expanded pattern
62
- return normalizeIP(value).some(ip =>
158
+ return normalizeIP(value).some(ip =>
63
159
  expandedPatterns.some(pattern => plugins.minimatch(ip, pattern))
64
160
  );
65
161
  };
66
162
 
67
- const findMatchingDomain = (serverName: string): DomainConfig | undefined => {
163
+ const findMatchingDomain = (serverName: string): IDomainConfig | undefined => {
68
164
  return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
69
165
  };
70
166
 
71
- const server = this.settings.sniEnabled
72
- ? plugins.tls.createServer({
73
- SNICallback: (serverName: string, cb: (err: Error | null, ctx?: plugins.tls.SecureContext) => void) => {
74
- console.log(`SNI request for domain: ${serverName}`);
75
- // Create a minimal context just to read SNI, we'll pass through the actual TLS
76
- const ctx = plugins.tls.createSecureContext({
77
- minVersion: 'TLSv1.2',
78
- maxVersion: 'TLSv1.3'
79
- });
80
- cb(null, ctx);
167
+ // Create a plain net server for TLS passthrough.
168
+ this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
169
+ const remoteIP = socket.remoteAddress || '';
170
+
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}`);
81
184
  }
82
- })
83
- : plugins.net.createServer();
84
-
85
- const handleConnection = (from: plugins.net.Socket | plugins.tls.TLSSocket) => {
86
- const remoteIP = from.remoteAddress || '';
87
- let serverName = '';
88
-
89
- // First check if this IP is in the default allowed list
90
- const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
91
-
92
- if (this.settings.sniEnabled && from instanceof plugins.tls.TLSSocket) {
93
- serverName = (from as any).servername || '';
94
- console.log(`TLS Connection from ${remoteIP} for domain: ${serverName}`);
95
- }
185
+ }
186
+ };
96
187
 
97
- // If IP is in defaultAllowedIPs, allow the connection regardless of SNI
98
- if (isDefaultAllowed) {
99
- console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
100
- } else if (this.settings.sniEnabled && serverName) {
101
- // For SNI connections that aren't in default list, check domain-specific rules
102
- const domainConfig = findMatchingDomain(serverName);
103
- if (!domainConfig) {
104
- console.log(`Connection rejected: No matching domain config for ${serverName} from IP ${remoteIP}`);
105
- from.end();
106
- return;
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}`);
107
196
  }
108
- if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
109
- console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
110
- from.end();
197
+ cleanupOnce();
198
+ };
199
+
200
+ const handleClose = (side: 'incoming' | 'outgoing') => () => {
201
+ console.log(`Connection closed on ${side} side from ${remoteIP}`);
202
+ cleanupOnce();
203
+ };
204
+
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}`);
213
+ socket.end();
214
+ return;
215
+ }
216
+ if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
217
+ console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
218
+ socket.end();
219
+ return;
220
+ }
221
+ } else if (!isDefaultAllowed && !serverName) {
222
+ console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
223
+ socket.end();
111
224
  return;
225
+ } else {
226
+ console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
112
227
  }
113
- } else {
114
- // Non-SNI connection and not in default list
115
- console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
116
- from.end();
117
- return;
118
- }
119
228
 
120
- // Determine target host - use domain-specific targetIP if available
121
- const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
122
- const targetHost = domainConfig?.targetIP || this.settings.toHost!;
123
-
124
- // Create connection, optionally preserving the client's source IP
125
- const connectionOptions: plugins.net.NetConnectOpts = {
126
- host: targetHost,
127
- port: this.settings.toPort,
229
+ // Determine target host.
230
+ const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
231
+ const targetHost = domainConfig?.targetIP || this.settings.toHost!;
232
+
233
+ // Create connection options.
234
+ const connectionOptions: plugins.net.NetConnectOpts = {
235
+ host: targetHost,
236
+ port: this.settings.toPort,
237
+ };
238
+ if (this.settings.preserveSourceIP) {
239
+ connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
240
+ }
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
+ }
250
+ socket.setTimeout(120000);
251
+ socket.pipe(to);
252
+ to.pipe(socket);
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'));
128
263
  };
129
264
 
130
- // Only set localAddress if preserveSourceIP is enabled
131
- if (this.settings.preserveSourceIP) {
132
- connectionOptions.localAddress = remoteIP.replace('::ffff:', ''); // Remove IPv6 mapping if present
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('');
133
281
  }
282
+ })
283
+ .on('error', (err: Error) => {
284
+ console.log(`Server Error: ${err.message}`);
285
+ })
286
+ .listen(this.settings.fromPort, () => {
287
+ console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
288
+ });
134
289
 
135
- // If this is a TLS connection, use net.connect to ensure raw passthrough
136
- const to = plugins.net.connect(connectionOptions);
137
- console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
138
- from.setTimeout(120000);
139
- from.pipe(to);
140
- to.pipe(from);
141
- from.on('error', () => {
142
- cleanUpSockets(from, to);
143
- });
144
- to.on('error', () => {
145
- cleanUpSockets(from, to);
146
- });
147
- from.on('close', () => {
148
- cleanUpSockets(from, to);
149
- });
150
- to.on('close', () => {
151
- cleanUpSockets(from, to);
152
- });
153
- from.on('timeout', () => {
154
- cleanUpSockets(from, to);
155
- });
156
- to.on('timeout', () => {
157
- cleanUpSockets(from, to);
158
- });
159
- from.on('end', () => {
160
- cleanUpSockets(from, to);
161
- });
162
- to.on('end', () => {
163
- cleanUpSockets(from, to);
164
- });
165
- };
166
-
167
- this.netServer = server
168
- .on('connection', handleConnection)
169
- .on('secureConnection', handleConnection)
170
- .on('tlsClientError', (err, tlsSocket) => {
171
- console.log(`TLS Client Error: ${err.message}`);
172
- })
173
- .on('error', (err) => {
174
- console.log(`Server Error: ${err.message}`);
175
- })
176
- .listen(this.settings.fromPort);
177
- console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI enabled)' : ''}`);
290
+ // Log active connection count every 10 seconds.
291
+ this.connectionLogger = setInterval(() => {
292
+ console.log(`(Interval Log) Active connections: ${this.activeConnections.size}`);
293
+ }, 10000);
178
294
  }
179
295
 
180
296
  public async stop() {
@@ -182,6 +298,10 @@ export class PortProxy {
182
298
  this.netServer.close(() => {
183
299
  done.resolve();
184
300
  });
301
+ if (this.connectionLogger) {
302
+ clearInterval(this.connectionLogger);
303
+ this.connectionLogger = null;
304
+ }
185
305
  await done.promise;
186
306
  }
187
- }
307
+ }