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