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