@push.rocks/smartproxy 3.7.2 → 3.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '3.7.2',
6
+ version: '3.7.3',
7
7
  description: 'a proxy for handling high workloads of proxying'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLGlEQUFpRDtDQUMvRCxDQUFBIn0=
@@ -1,22 +1,22 @@
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
+ constructor(settings: IProxySettings);
20
20
  start(): Promise<void>;
21
21
  stop(): Promise<void>;
22
22
  }
@@ -1,4 +1,88 @@
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) {
4
88
  this.settings = {
@@ -24,7 +108,7 @@ export class PortProxy {
24
108
  return [ip, ipv4];
25
109
  }
26
110
  // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
27
- if (ip.match(/^\d{1,3}(\.\d{1,3}){3}$/)) {
111
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
28
112
  return [ip, `::ffff:${ip}`];
29
113
  }
30
114
  return [ip];
@@ -38,106 +122,108 @@ export class PortProxy {
38
122
  const findMatchingDomain = (serverName) => {
39
123
  return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
40
124
  };
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);
51
- }
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`);
125
+ // Always create a plain net server for TLS passthrough.
126
+ this.netServer = plugins.net.createServer((socket) => {
127
+ 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
+ }
148
+ }
149
+ else if (!isDefaultAllowed && !serverName) {
150
+ console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
151
+ socket.end();
152
+ return;
153
+ }
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:', '');
167
+ }
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
+ });
66
187
  }
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();
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`);
192
+ socket.end();
73
193
  return;
74
194
  }
75
- if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
76
- console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
77
- from.end();
78
- return;
195
+ const targetHost = this.settings.toHost;
196
+ const connectionOptions = {
197
+ host: targetHost,
198
+ port: this.settings.toPort,
199
+ };
200
+ if (this.settings.preserveSourceIP) {
201
+ connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
79
202
  }
203
+ const to = plugins.net.connect(connectionOptions);
204
+ console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}`);
205
+ socket.setTimeout(120000);
206
+ socket.pipe(to);
207
+ 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);
80
219
  }
81
- 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
98
- }
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
220
  })
136
221
  .on('error', (err) => {
137
222
  console.log(`Server Error: ${err.message}`);
138
223
  })
139
- .listen(this.settings.fromPort);
140
- console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI enabled)' : ''}`);
224
+ .listen(this.settings.fromPort, () => {
225
+ console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
226
+ });
141
227
  }
142
228
  async stop() {
143
229
  const done = plugins.smartpromise.defer();
@@ -147,4 +233,4 @@ export class PortProxy {
147
233
  await done.promise;
148
234
  }
149
235
  }
150
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic21hcnRwcm94eS5wb3J0cHJveHkuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9zbWFydHByb3h5LnBvcnRwcm94eS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLHlCQUF5QixDQUFDO0FBc0JuRCxNQUFNLE9BQU8sU0FBUztJQUlwQixZQUFZLFFBQXVCO1FBQ2pDLElBQUksQ0FBQyxRQUFRLEdBQUc7WUFDZCxHQUFHLFFBQVE7WUFDWCxNQUFNLEVBQUUsUUFBUSxDQUFDLE1BQU0sSUFBSSxXQUFXO1NBQ3ZDLENBQUM7SUFDSixDQUFDO0lBRU0sS0FBSyxDQUFDLEtBQUs7UUFDaEIsTUFBTSxjQUFjLEdBQUcsQ0FBQyxJQUF3QixFQUFFLEVBQXNCLEVBQUUsRUFBRTtZQUMxRSxJQUFJLENBQUMsR0FBRyxFQUFFLENBQUM7WUFDWCxFQUFFLENBQUMsR0FBRyxFQUFFLENBQUM7WUFDVCxJQUFJLENBQUMsa0JBQWtCLEVBQUUsQ0FBQztZQUMxQixFQUFFLENBQUMsa0JBQWtCLEVBQUUsQ0FBQztZQUN4QixJQUFJLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDZCxFQUFFLENBQUMsTUFBTSxFQUFFLENBQUM7WUFDWixJQUFJLENBQUMsT0FBTyxFQUFFLENBQUM7WUFDZixFQUFFLENBQUMsT0FBTyxFQUFFLENBQUM7UUFDZixDQUFDLENBQUM7UUFDRixNQUFNLFdBQVcsR0FBRyxDQUFDLEVBQVUsRUFBWSxFQUFFO1lBQzNDLG9DQUFvQztZQUNwQyxJQUFJLEVBQUUsQ0FBQyxVQUFVLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztnQkFDN0IsTUFBTSxJQUFJLEdBQUcsRUFBRSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLDBCQUEwQjtnQkFDcEQsT0FBTyxDQUFDLEVBQUUsRUFBRSxJQUFJLENBQUMsQ0FBQztZQUNwQixDQUFDO1lBQ0QsMkRBQTJEO1lBQzNELElBQUksRUFBRSxDQUFD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
236
+ //# 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.7.3",
4
4
  "private": false,
5
5
  "description": "a proxy for handling high workloads of proxying",
6
6
  "main": "dist_ts/index.js",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '3.7.2',
6
+ version: '3.7.3',
7
7
  description: 'a proxy for handling high workloads of proxying'
8
8
  }
@@ -1,30 +1,122 @@
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;
26
118
 
27
- constructor(settings: ProxySettings) {
119
+ constructor(settings: IProxySettings) {
28
120
  this.settings = {
29
121
  ...settings,
30
122
  toHost: settings.toHost || 'localhost'
@@ -42,6 +134,7 @@ export class PortProxy {
42
134
  from.destroy();
43
135
  to.destroy();
44
136
  };
137
+
45
138
  const normalizeIP = (ip: string): string[] => {
46
139
  // Handle IPv4-mapped IPv6 addresses
47
140
  if (ip.startsWith('::ffff:')) {
@@ -49,7 +142,7 @@ export class PortProxy {
49
142
  return [ip, ipv4];
50
143
  }
51
144
  // Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
52
- if (ip.match(/^\d{1,3}(\.\d{1,3}){3}$/)) {
145
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
53
146
  return [ip, `::ffff:${ip}`];
54
147
  }
55
148
  return [ip];
@@ -59,122 +152,121 @@ export class PortProxy {
59
152
  // Expand patterns to include both IPv4 and IPv6 variants
60
153
  const expandedPatterns = patterns.flatMap(normalizeIP);
61
154
  // Check if any variant of the IP matches any expanded pattern
62
- return normalizeIP(value).some(ip =>
155
+ return normalizeIP(value).some(ip =>
63
156
  expandedPatterns.some(pattern => plugins.minimatch(ip, pattern))
64
157
  );
65
158
  };
66
159
 
67
- const findMatchingDomain = (serverName: string): DomainConfig | undefined => {
160
+ const findMatchingDomain = (serverName: string): IDomainConfig | undefined => {
68
161
  return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
69
162
  };
70
163
 
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);
164
+ // Always create a plain net server for TLS passthrough.
165
+ this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
166
+ const remoteIP = socket.remoteAddress || '';
167
+
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}`);
174
+
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`);
191
+ socket.end();
192
+ return;
193
+ } else {
194
+ console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
81
195
  }
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
- }
96
196
 
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();
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:', '');
208
+ }
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`);
235
+ socket.end();
106
236
  return;
107
237
  }
108
- if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
109
- console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
110
- from.end();
111
- return;
238
+ const targetHost = this.settings.toHost!;
239
+ const connectionOptions: plugins.net.NetConnectOpts = {
240
+ host: targetHost,
241
+ port: this.settings.toPort,
242
+ };
243
+ if (this.settings.preserveSourceIP) {
244
+ connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
112
245
  }
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
-
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,
128
- };
129
-
130
- // Only set localAddress if preserveSourceIP is enabled
131
- if (this.settings.preserveSourceIP) {
132
- connectionOptions.localAddress = remoteIP.replace('::ffff:', ''); // Remove IPv6 mapping if present
246
+ const to = plugins.net.connect(connectionOptions);
247
+ console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}`);
248
+ socket.setTimeout(120000);
249
+ socket.pipe(to);
250
+ 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);
133
262
  }
134
-
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)' : ''}`);
263
+ })
264
+ .on('error', (err: Error) => {
265
+ console.log(`Server Error: ${err.message}`);
266
+ })
267
+ .listen(this.settings.fromPort, () => {
268
+ console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
269
+ });
178
270
  }
179
271
 
180
272
  public async stop() {
@@ -184,4 +276,4 @@ export class PortProxy {
184
276
  });
185
277
  await done.promise;
186
278
  }
187
- }
279
+ }