@push.rocks/smartproxy 3.10.1 → 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 +105 -174
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/smartproxy.portproxy.ts +123 -196
|
@@ -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,46 +204,49 @@ 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, peek at the first chunk.
|
|
307
242
|
if (this.settings.sniEnabled) {
|
|
243
|
+
socket.setTimeout(5000, () => {
|
|
244
|
+
console.log(`Initial data timeout for ${remoteIP}`);
|
|
245
|
+
socket.end();
|
|
246
|
+
cleanupOnce();
|
|
247
|
+
});
|
|
308
248
|
socket.once('data', (chunk) => {
|
|
249
|
+
socket.setTimeout(0);
|
|
309
250
|
initialDataReceived = true;
|
|
310
251
|
const serverName = extractSNI(chunk) || '';
|
|
311
252
|
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
|
|
@@ -313,17 +254,9 @@ export class PortProxy {
|
|
|
313
254
|
});
|
|
314
255
|
}
|
|
315
256
|
else {
|
|
316
|
-
// For non-SNI connections, simply check defaultAllowedIPs.
|
|
317
257
|
initialDataReceived = true;
|
|
318
258
|
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
319
|
-
|
|
320
|
-
socket.end();
|
|
321
|
-
if (incomingTermReason === null) {
|
|
322
|
-
incomingTermReason = 'rejected';
|
|
323
|
-
this.incrementTerminationStat('incoming', 'rejected');
|
|
324
|
-
}
|
|
325
|
-
cleanupOnce();
|
|
326
|
-
return;
|
|
259
|
+
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
|
327
260
|
}
|
|
328
261
|
setupConnection('');
|
|
329
262
|
}
|
|
@@ -332,7 +265,8 @@ export class PortProxy {
|
|
|
332
265
|
console.log(`Server Error: ${err.message}`);
|
|
333
266
|
})
|
|
334
267
|
.listen(this.settings.fromPort, () => {
|
|
335
|
-
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)' : ''}`);
|
|
336
270
|
});
|
|
337
271
|
// Log active connection count, longest running connection durations,
|
|
338
272
|
// and termination statistics every 10 seconds.
|
|
@@ -340,19 +274,16 @@ export class PortProxy {
|
|
|
340
274
|
const now = Date.now();
|
|
341
275
|
let maxIncoming = 0;
|
|
342
276
|
for (const startTime of this.incomingConnectionTimes.values()) {
|
|
343
|
-
|
|
344
|
-
if (duration > maxIncoming) {
|
|
345
|
-
maxIncoming = duration;
|
|
346
|
-
}
|
|
277
|
+
maxIncoming = Math.max(maxIncoming, now - startTime);
|
|
347
278
|
}
|
|
348
279
|
let maxOutgoing = 0;
|
|
349
280
|
for (const startTime of this.outgoingConnectionTimes.values()) {
|
|
350
|
-
|
|
351
|
-
if (duration > maxOutgoing) {
|
|
352
|
-
maxOutgoing = duration;
|
|
353
|
-
}
|
|
281
|
+
maxOutgoing = Math.max(maxOutgoing, now - startTime);
|
|
354
282
|
}
|
|
355
|
-
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)}`);
|
|
356
287
|
}, 10000);
|
|
357
288
|
}
|
|
358
289
|
async stop() {
|
|
@@ -367,4 +298,4 @@ export class PortProxy {
|
|
|
367
298
|
await done.promise;
|
|
368
299
|
}
|
|
369
300
|
}
|
|
370
|
-
//# 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;
|
|
227
|
+
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
|
|
291
228
|
}
|
|
292
|
-
} else if (!
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (incomingTermReason === null) {
|
|
296
|
-
incomingTermReason = 'rejected';
|
|
297
|
-
this.incrementTerminationStat('incoming', 'rejected');
|
|
298
|
-
}
|
|
299
|
-
cleanupOnce();
|
|
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,76 +242,77 @@ 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, peek at the first chunk.
|
|
359
285
|
if (this.settings.sniEnabled) {
|
|
286
|
+
socket.setTimeout(5000, () => {
|
|
287
|
+
console.log(`Initial data timeout for ${remoteIP}`);
|
|
288
|
+
socket.end();
|
|
289
|
+
cleanupOnce();
|
|
290
|
+
});
|
|
291
|
+
|
|
360
292
|
socket.once('data', (chunk: Buffer) => {
|
|
293
|
+
socket.setTimeout(0);
|
|
361
294
|
initialDataReceived = true;
|
|
362
295
|
const serverName = extractSNI(chunk) || '';
|
|
363
296
|
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
|
|
364
297
|
setupConnection(serverName, chunk);
|
|
365
298
|
});
|
|
366
299
|
} else {
|
|
367
|
-
// For non-SNI connections, simply check defaultAllowedIPs.
|
|
368
300
|
initialDataReceived = true;
|
|
369
301
|
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
370
|
-
|
|
371
|
-
socket.end();
|
|
372
|
-
if (incomingTermReason === null) {
|
|
373
|
-
incomingTermReason = 'rejected';
|
|
374
|
-
this.incrementTerminationStat('incoming', 'rejected');
|
|
375
|
-
}
|
|
376
|
-
cleanupOnce();
|
|
377
|
-
return;
|
|
302
|
+
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
|
378
303
|
}
|
|
379
304
|
setupConnection('');
|
|
380
305
|
}
|
|
381
306
|
})
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
});
|
|
388
316
|
|
|
389
317
|
// Log active connection count, longest running connection durations,
|
|
390
318
|
// and termination statistics every 10 seconds.
|
|
@@ -392,19 +320,18 @@ export class PortProxy {
|
|
|
392
320
|
const now = Date.now();
|
|
393
321
|
let maxIncoming = 0;
|
|
394
322
|
for (const startTime of this.incomingConnectionTimes.values()) {
|
|
395
|
-
|
|
396
|
-
if (duration > maxIncoming) {
|
|
397
|
-
maxIncoming = duration;
|
|
398
|
-
}
|
|
323
|
+
maxIncoming = Math.max(maxIncoming, now - startTime);
|
|
399
324
|
}
|
|
400
325
|
let maxOutgoing = 0;
|
|
401
326
|
for (const startTime of this.outgoingConnectionTimes.values()) {
|
|
402
|
-
|
|
403
|
-
if (duration > maxOutgoing) {
|
|
404
|
-
maxOutgoing = duration;
|
|
405
|
-
}
|
|
327
|
+
maxOutgoing = Math.max(maxOutgoing, now - startTime);
|
|
406
328
|
}
|
|
407
|
-
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
|
+
);
|
|
408
335
|
}, 10000);
|
|
409
336
|
}
|
|
410
337
|
|