@push.rocks/smartproxy 3.22.1 → 3.22.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/classes.portproxy.d.ts +6 -13
- package/dist_ts/classes.portproxy.js +99 -245
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +114 -281
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartproxy",
|
|
3
|
-
"version": "3.22.
|
|
3
|
+
"version": "3.22.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.",
|
|
6
6
|
"main": "dist_ts/index.js",
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@push.rocks/smartproxy',
|
|
6
|
-
version: '3.22.
|
|
6
|
+
version: '3.22.3',
|
|
7
7
|
description: 'A powerful proxy package that effectively handles high traffic, with features such as SSL/TLS support, port proxying, WebSocket handling, and dynamic routing with authentication options.'
|
|
8
8
|
}
|
package/ts/classes.portproxy.ts
CHANGED
|
@@ -23,7 +23,6 @@ export interface IPortProxySettings extends plugins.tls.TlsOptions {
|
|
|
23
23
|
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
24
24
|
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
|
|
25
25
|
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
|
|
26
|
-
initialDataTimeout?: number; // (ms) timeout for receiving initial data, useful for chained proxies
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
/**
|
|
@@ -90,25 +89,24 @@ function extractSNI(buffer: Buffer): string | undefined {
|
|
|
90
89
|
}
|
|
91
90
|
|
|
92
91
|
interface IConnectionRecord {
|
|
92
|
+
id: string; // Unique connection identifier
|
|
93
93
|
incoming: plugins.net.Socket;
|
|
94
94
|
outgoing: plugins.net.Socket | null;
|
|
95
95
|
incomingStartTime: number;
|
|
96
96
|
outgoingStartTime?: number;
|
|
97
97
|
outgoingClosedTime?: number;
|
|
98
|
-
lockedDomain?: string;
|
|
99
|
-
connectionClosed: boolean;
|
|
100
|
-
cleanupTimer?: NodeJS.Timeout;
|
|
101
|
-
|
|
102
|
-
id: string; // Unique identifier for the connection
|
|
103
|
-
lastActivity: number; // Timestamp of last activity on either socket
|
|
98
|
+
lockedDomain?: string; // Used to lock this connection to the initial SNI
|
|
99
|
+
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
|
|
100
|
+
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
|
|
101
|
+
lastActivity: number; // Last activity timestamp for inactivity detection
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
// Helper: Check if a port falls within any of the given port ranges
|
|
104
|
+
// Helper: Check if a port falls within any of the given port ranges
|
|
107
105
|
const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
|
|
108
106
|
return ranges.some(range => port >= range.from && port <= range.to);
|
|
109
107
|
};
|
|
110
108
|
|
|
111
|
-
// Helper: Check if a given IP matches any of the glob patterns
|
|
109
|
+
// Helper: Check if a given IP matches any of the glob patterns
|
|
112
110
|
const isAllowed = (ip: string, patterns: string[]): boolean => {
|
|
113
111
|
const normalizeIP = (ip: string): string[] => {
|
|
114
112
|
if (ip.startsWith('::ffff:')) {
|
|
@@ -127,13 +125,13 @@ const isAllowed = (ip: string, patterns: string[]): boolean => {
|
|
|
127
125
|
);
|
|
128
126
|
};
|
|
129
127
|
|
|
130
|
-
// Helper: Check if an IP is allowed considering allowed and blocked glob patterns
|
|
128
|
+
// Helper: Check if an IP is allowed considering allowed and blocked glob patterns
|
|
131
129
|
const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
|
|
132
130
|
if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
|
|
133
131
|
return isAllowed(ip, allowed);
|
|
134
132
|
};
|
|
135
133
|
|
|
136
|
-
// Helper: Generate a unique ID
|
|
134
|
+
// Helper: Generate a unique connection ID
|
|
137
135
|
const generateConnectionId = (): string => {
|
|
138
136
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
139
137
|
};
|
|
@@ -141,12 +139,11 @@ const generateConnectionId = (): string => {
|
|
|
141
139
|
export class PortProxy {
|
|
142
140
|
private netServers: plugins.net.Server[] = [];
|
|
143
141
|
settings: IPortProxySettings;
|
|
144
|
-
// Unified record tracking each connection pair.
|
|
145
142
|
private connectionRecords: Map<string, IConnectionRecord> = new Map();
|
|
146
143
|
private connectionLogger: NodeJS.Timeout | null = null;
|
|
147
144
|
private isShuttingDown: boolean = false;
|
|
148
145
|
|
|
149
|
-
// Map to track round robin indices for each domain config
|
|
146
|
+
// Map to track round robin indices for each domain config
|
|
150
147
|
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
|
|
151
148
|
|
|
152
149
|
private terminationStats: {
|
|
@@ -164,9 +161,6 @@ export class PortProxy {
|
|
|
164
161
|
maxConnectionLifetime: settingsArg.maxConnectionLifetime || 600000,
|
|
165
162
|
gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000,
|
|
166
163
|
};
|
|
167
|
-
|
|
168
|
-
// Debug logging for constructor settings
|
|
169
|
-
console.log(`PortProxy initialized with targetIP: ${this.settings.targetIP}, toPort: ${this.settings.toPort}, fromPort: ${this.settings.fromPort}, sniEnabled: ${this.settings.sniEnabled}`);
|
|
170
164
|
}
|
|
171
165
|
|
|
172
166
|
private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
|
|
@@ -174,77 +168,64 @@ export class PortProxy {
|
|
|
174
168
|
}
|
|
175
169
|
|
|
176
170
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (record.cleanupInitiated) return;
|
|
182
|
-
|
|
183
|
-
record.cleanupInitiated = true;
|
|
184
|
-
const remoteIP = record.incoming.remoteAddress || 'unknown';
|
|
185
|
-
console.log(`Initiating cleanup for connection ${record.id} from ${remoteIP} (reason: ${reason})`);
|
|
186
|
-
|
|
187
|
-
// Execute cleanup immediately to prevent lingering connections
|
|
188
|
-
this.executeCleanup(record);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Executes the actual cleanup of a connection.
|
|
193
|
-
* Destroys sockets, clears timers, and removes the record.
|
|
171
|
+
* Cleans up a connection record.
|
|
172
|
+
* Destroys both incoming and outgoing sockets, clears timers, and removes the record.
|
|
173
|
+
* @param record - The connection record to clean up
|
|
174
|
+
* @param reason - Optional reason for cleanup (for logging)
|
|
194
175
|
*/
|
|
195
|
-
private
|
|
196
|
-
if (record.connectionClosed)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
clearTimeout(record.cleanupTimer);
|
|
203
|
-
record.cleanupTimer = undefined;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// End the sockets first to allow for graceful closure
|
|
207
|
-
try {
|
|
208
|
-
if (!record.incoming.destroyed) {
|
|
209
|
-
record.incoming.end();
|
|
210
|
-
// Set a safety timeout to force destroy if end doesn't complete
|
|
211
|
-
setTimeout(() => {
|
|
212
|
-
if (!record.incoming.destroyed) {
|
|
213
|
-
console.log(`Forcing destruction of incoming socket for ${remoteIP}`);
|
|
214
|
-
record.incoming.destroy();
|
|
215
|
-
}
|
|
216
|
-
}, 1000);
|
|
217
|
-
}
|
|
218
|
-
} catch (err) {
|
|
219
|
-
console.error(`Error ending incoming socket for ${remoteIP}:`, err);
|
|
220
|
-
if (!record.incoming.destroyed) {
|
|
221
|
-
record.incoming.destroy();
|
|
176
|
+
private cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
|
|
177
|
+
if (!record.connectionClosed) {
|
|
178
|
+
record.connectionClosed = true;
|
|
179
|
+
|
|
180
|
+
if (record.cleanupTimer) {
|
|
181
|
+
clearTimeout(record.cleanupTimer);
|
|
182
|
+
record.cleanupTimer = undefined;
|
|
222
183
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
if (!record.incoming.destroyed) {
|
|
187
|
+
// Try graceful shutdown first, then force destroy after a short timeout
|
|
188
|
+
record.incoming.end();
|
|
189
|
+
setTimeout(() => {
|
|
190
|
+
if (record && !record.incoming.destroyed) {
|
|
191
|
+
record.incoming.destroy();
|
|
192
|
+
}
|
|
193
|
+
}, 1000);
|
|
194
|
+
}
|
|
195
|
+
} catch (err) {
|
|
196
|
+
console.log(`Error closing incoming socket: ${err}`);
|
|
197
|
+
if (!record.incoming.destroyed) {
|
|
198
|
+
record.incoming.destroy();
|
|
199
|
+
}
|
|
235
200
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
204
|
+
// Try graceful shutdown first, then force destroy after a short timeout
|
|
205
|
+
record.outgoing.end();
|
|
206
|
+
setTimeout(() => {
|
|
207
|
+
if (record && record.outgoing && !record.outgoing.destroyed) {
|
|
208
|
+
record.outgoing.destroy();
|
|
209
|
+
}
|
|
210
|
+
}, 1000);
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.log(`Error closing outgoing socket: ${err}`);
|
|
214
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
215
|
+
record.outgoing.destroy();
|
|
216
|
+
}
|
|
240
217
|
}
|
|
218
|
+
|
|
219
|
+
// Remove the record from the tracking map
|
|
220
|
+
this.connectionRecords.delete(record.id);
|
|
221
|
+
|
|
222
|
+
const remoteIP = record.incoming.remoteAddress || 'unknown';
|
|
223
|
+
console.log(`Connection from ${remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`);
|
|
241
224
|
}
|
|
225
|
+
}
|
|
242
226
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
this.connectionRecords.delete(record.id);
|
|
246
|
-
console.log(`Connection ${record.id} from ${remoteIP} fully cleaned up. Active connections: ${this.connectionRecords.size}`);
|
|
247
|
-
}, 2000);
|
|
227
|
+
private updateActivity(record: IConnectionRecord): void {
|
|
228
|
+
record.lastActivity = Date.now();
|
|
248
229
|
}
|
|
249
230
|
|
|
250
231
|
private getTargetIP(domainConfig: IDomainConfig): string {
|
|
@@ -257,27 +238,6 @@ export class PortProxy {
|
|
|
257
238
|
return this.settings.targetIP!;
|
|
258
239
|
}
|
|
259
240
|
|
|
260
|
-
/**
|
|
261
|
-
* Updates the last activity timestamp for a connection record
|
|
262
|
-
*/
|
|
263
|
-
private updateActivity(record: IConnectionRecord): void {
|
|
264
|
-
record.lastActivity = Date.now();
|
|
265
|
-
|
|
266
|
-
// Reset the inactivity timer if one is set
|
|
267
|
-
if (this.settings.maxConnectionLifetime && record.cleanupTimer) {
|
|
268
|
-
clearTimeout(record.cleanupTimer);
|
|
269
|
-
|
|
270
|
-
// Set a new cleanup timer
|
|
271
|
-
record.cleanupTimer = setTimeout(() => {
|
|
272
|
-
const now = Date.now();
|
|
273
|
-
const inactivityTime = now - record.lastActivity;
|
|
274
|
-
const remoteIP = record.incoming.remoteAddress || 'unknown';
|
|
275
|
-
console.log(`Connection ${record.id} from ${remoteIP} exceeded max lifetime or inactivity period (${inactivityTime}ms), forcing cleanup.`);
|
|
276
|
-
this.initiateCleanup(record, 'timeout');
|
|
277
|
-
}, this.settings.maxConnectionLifetime);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
241
|
public async start() {
|
|
282
242
|
// Define a unified connection handler for all listening ports.
|
|
283
243
|
const connectionHandler = (socket: plugins.net.Socket) => {
|
|
@@ -297,23 +257,28 @@ export class PortProxy {
|
|
|
297
257
|
outgoing: null,
|
|
298
258
|
incomingStartTime: Date.now(),
|
|
299
259
|
lastActivity: Date.now(),
|
|
300
|
-
connectionClosed: false
|
|
301
|
-
cleanupInitiated: false
|
|
260
|
+
connectionClosed: false
|
|
302
261
|
};
|
|
303
|
-
|
|
304
262
|
this.connectionRecords.set(connectionId, connectionRecord);
|
|
305
|
-
|
|
263
|
+
|
|
264
|
+
console.log(`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`);
|
|
306
265
|
|
|
307
266
|
let initialDataReceived = false;
|
|
308
267
|
let incomingTerminationReason: string | null = null;
|
|
309
268
|
let outgoingTerminationReason: string | null = null;
|
|
310
269
|
|
|
311
|
-
// Local
|
|
270
|
+
// Local function for cleanupOnce
|
|
271
|
+
const cleanupOnce = () => {
|
|
272
|
+
this.cleanupConnection(connectionRecord);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Define initiateCleanupOnce for compatibility with potential future improvements
|
|
312
276
|
const initiateCleanupOnce = (reason: string = 'normal') => {
|
|
313
|
-
|
|
277
|
+
console.log(`Connection cleanup initiated for ${remoteIP} (${reason})`);
|
|
278
|
+
cleanupOnce();
|
|
314
279
|
};
|
|
315
280
|
|
|
316
|
-
// Helper to reject an incoming connection
|
|
281
|
+
// Helper to reject an incoming connection
|
|
317
282
|
const rejectIncomingConnection = (reason: string, logMessage: string) => {
|
|
318
283
|
console.log(logMessage);
|
|
319
284
|
socket.end();
|
|
@@ -321,43 +286,17 @@ export class PortProxy {
|
|
|
321
286
|
incomingTerminationReason = reason;
|
|
322
287
|
this.incrementTerminationStat('incoming', reason);
|
|
323
288
|
}
|
|
324
|
-
|
|
289
|
+
cleanupOnce();
|
|
325
290
|
};
|
|
326
291
|
|
|
327
|
-
//
|
|
328
|
-
// The code below is commented out to restore original behavior
|
|
329
|
-
/*
|
|
330
|
-
let initialTimeout: NodeJS.Timeout | null = null;
|
|
331
|
-
const initialTimeoutMs = this.settings.initialDataTimeout ||
|
|
332
|
-
(this.settings.sniEnabled ? 15000 : 0);
|
|
333
|
-
|
|
334
|
-
if (initialTimeoutMs > 0) {
|
|
335
|
-
console.log(`Setting initial data timeout of ${initialTimeoutMs}ms for connection from ${remoteIP}`);
|
|
336
|
-
initialTimeout = setTimeout(() => {
|
|
337
|
-
if (!initialDataReceived) {
|
|
338
|
-
console.log(`Initial connection timeout for ${remoteIP} (no data received after ${initialTimeoutMs}ms)`);
|
|
339
|
-
if (incomingTerminationReason === null) {
|
|
340
|
-
incomingTerminationReason = 'initial_timeout';
|
|
341
|
-
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
342
|
-
}
|
|
343
|
-
initiateCleanupOnce('initial_timeout');
|
|
344
|
-
}
|
|
345
|
-
}, initialTimeoutMs);
|
|
346
|
-
} else {
|
|
347
|
-
console.log(`No initial timeout set for connection from ${remoteIP} (likely chained proxy)`);
|
|
348
|
-
initialDataReceived = true;
|
|
349
|
-
}
|
|
350
|
-
*/
|
|
351
|
-
|
|
352
|
-
// Original behavior: only set timeout if SNI is enabled, and use a fixed 5 second timeout
|
|
292
|
+
// Set an initial timeout for SNI data if needed
|
|
353
293
|
let initialTimeout: NodeJS.Timeout | null = null;
|
|
354
294
|
if (this.settings.sniEnabled) {
|
|
355
|
-
console.log(`Setting 5 second initial timeout for SNI extraction from ${remoteIP}`);
|
|
356
295
|
initialTimeout = setTimeout(() => {
|
|
357
296
|
if (!initialDataReceived) {
|
|
358
297
|
console.log(`Initial data timeout for ${remoteIP}`);
|
|
359
298
|
socket.end();
|
|
360
|
-
|
|
299
|
+
cleanupOnce();
|
|
361
300
|
}
|
|
362
301
|
}, 5000);
|
|
363
302
|
} else {
|
|
@@ -365,26 +304,7 @@ export class PortProxy {
|
|
|
365
304
|
}
|
|
366
305
|
|
|
367
306
|
socket.on('error', (err: Error) => {
|
|
368
|
-
|
|
369
|
-
? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
|
|
370
|
-
: `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
|
|
371
|
-
console.log(errorMessage);
|
|
372
|
-
|
|
373
|
-
// Clear the initial timeout if it exists
|
|
374
|
-
if (initialTimeout) {
|
|
375
|
-
clearTimeout(initialTimeout);
|
|
376
|
-
initialTimeout = null;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// For premature errors, we need to handle them explicitly
|
|
380
|
-
// since the standard error handlers might not be set up yet
|
|
381
|
-
if (!initialDataReceived) {
|
|
382
|
-
if (incomingTerminationReason === null) {
|
|
383
|
-
incomingTerminationReason = 'premature_error';
|
|
384
|
-
this.incrementTerminationStat('incoming', 'premature_error');
|
|
385
|
-
}
|
|
386
|
-
initiateCleanupOnce('premature_error');
|
|
387
|
-
}
|
|
307
|
+
console.log(`Incoming socket error from ${remoteIP}: ${err.message}`);
|
|
388
308
|
});
|
|
389
309
|
|
|
390
310
|
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
|
@@ -393,13 +313,9 @@ export class PortProxy {
|
|
|
393
313
|
if (code === 'ECONNRESET') {
|
|
394
314
|
reason = 'econnreset';
|
|
395
315
|
console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
|
|
396
|
-
} else if (code === 'ECONNREFUSED') {
|
|
397
|
-
reason = 'econnrefused';
|
|
398
|
-
console.log(`ECONNREFUSED on ${side} side from ${remoteIP}: ${err.message}`);
|
|
399
316
|
} else {
|
|
400
317
|
console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
|
|
401
318
|
}
|
|
402
|
-
|
|
403
319
|
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
404
320
|
incomingTerminationReason = reason;
|
|
405
321
|
this.incrementTerminationStat('incoming', reason);
|
|
@@ -407,13 +323,11 @@ export class PortProxy {
|
|
|
407
323
|
outgoingTerminationReason = reason;
|
|
408
324
|
this.incrementTerminationStat('outgoing', reason);
|
|
409
325
|
}
|
|
410
|
-
|
|
411
326
|
initiateCleanupOnce(reason);
|
|
412
327
|
};
|
|
413
328
|
|
|
414
329
|
const handleClose = (side: 'incoming' | 'outgoing') => () => {
|
|
415
330
|
console.log(`Connection closed on ${side} side from ${remoteIP}`);
|
|
416
|
-
|
|
417
331
|
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
418
332
|
incomingTerminationReason = 'normal';
|
|
419
333
|
this.incrementTerminationStat('incoming', 'normal');
|
|
@@ -422,24 +336,8 @@ export class PortProxy {
|
|
|
422
336
|
this.incrementTerminationStat('outgoing', 'normal');
|
|
423
337
|
// Record the time when outgoing socket closed.
|
|
424
338
|
connectionRecord.outgoingClosedTime = Date.now();
|
|
425
|
-
|
|
426
|
-
// If incoming is still active but outgoing closed, set a shorter timeout
|
|
427
|
-
if (!connectionRecord.incoming.destroyed) {
|
|
428
|
-
console.log(`Outgoing socket closed but incoming still active for ${remoteIP}. Setting cleanup timeout.`);
|
|
429
|
-
setTimeout(() => {
|
|
430
|
-
if (!connectionRecord.connectionClosed && !connectionRecord.incoming.destroyed) {
|
|
431
|
-
console.log(`Incoming socket still active ${Date.now() - connectionRecord.outgoingClosedTime!}ms after outgoing closed for ${remoteIP}. Cleaning up.`);
|
|
432
|
-
initiateCleanupOnce('outgoing_closed_timeout');
|
|
433
|
-
}
|
|
434
|
-
}, 10000); // 10 second timeout instead of waiting for the next parity check
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// If both sides are closed/destroyed, clean up
|
|
439
|
-
if ((side === 'incoming' && connectionRecord.outgoing?.destroyed) ||
|
|
440
|
-
(side === 'outgoing' && connectionRecord.incoming.destroyed)) {
|
|
441
|
-
initiateCleanupOnce('both_closed');
|
|
442
339
|
}
|
|
340
|
+
initiateCleanupOnce('closed_' + side);
|
|
443
341
|
};
|
|
444
342
|
|
|
445
343
|
/**
|
|
@@ -453,6 +351,7 @@ export class PortProxy {
|
|
|
453
351
|
// Clear the initial timeout since we've received data
|
|
454
352
|
if (initialTimeout) {
|
|
455
353
|
clearTimeout(initialTimeout);
|
|
354
|
+
initialTimeout = null;
|
|
456
355
|
}
|
|
457
356
|
|
|
458
357
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
|
@@ -462,9 +361,7 @@ export class PortProxy {
|
|
|
462
361
|
config.domains.some(d => plugins.minimatch(serverName, d))
|
|
463
362
|
) : undefined);
|
|
464
363
|
|
|
465
|
-
//
|
|
466
|
-
// Use original domain configuration and IP validation logic
|
|
467
|
-
// This restores the behavior that was working before
|
|
364
|
+
// IP validation is skipped if allowedIPs is empty
|
|
468
365
|
if (domainConfig) {
|
|
469
366
|
const effectiveAllowedIPs: string[] = [
|
|
470
367
|
...domainConfig.allowedIPs,
|
|
@@ -475,7 +372,7 @@ export class PortProxy {
|
|
|
475
372
|
...(this.settings.defaultBlockedIPs || [])
|
|
476
373
|
];
|
|
477
374
|
|
|
478
|
-
//
|
|
375
|
+
// Skip IP validation if allowedIPs is empty
|
|
479
376
|
if (domainConfig.allowedIPs.length > 0 && !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
480
377
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
|
|
481
378
|
}
|
|
@@ -483,8 +380,7 @@ export class PortProxy {
|
|
|
483
380
|
if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
|
|
484
381
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
|
|
485
382
|
}
|
|
486
|
-
}
|
|
487
|
-
// If no IP validation rules, allow the connection (original behavior)
|
|
383
|
+
}
|
|
488
384
|
|
|
489
385
|
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
|
490
386
|
const connectionOptions: plugins.net.NetConnectOpts = {
|
|
@@ -495,116 +391,57 @@ export class PortProxy {
|
|
|
495
391
|
connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
|
|
496
392
|
}
|
|
497
393
|
|
|
498
|
-
//
|
|
499
|
-
let connectionTimeout: NodeJS.Timeout | null = null;
|
|
500
|
-
let connectionSucceeded = false;
|
|
501
|
-
|
|
502
|
-
// Set connection timeout - longer for chained proxies
|
|
503
|
-
connectionTimeout = setTimeout(() => {
|
|
504
|
-
if (!connectionSucceeded) {
|
|
505
|
-
console.log(`Connection timeout connecting to ${targetHost}:${connectionOptions.port} for ${remoteIP}`);
|
|
506
|
-
if (outgoingTerminationReason === null) {
|
|
507
|
-
outgoingTerminationReason = 'connection_timeout';
|
|
508
|
-
this.incrementTerminationStat('outgoing', 'connection_timeout');
|
|
509
|
-
}
|
|
510
|
-
initiateCleanupOnce('connection_timeout');
|
|
511
|
-
}
|
|
512
|
-
}, 10000); // Increased from 5s to 10s to accommodate chained proxies
|
|
513
|
-
|
|
514
|
-
console.log(`Attempting to connect to ${targetHost}:${connectionOptions.port} for client ${remoteIP}...`);
|
|
515
|
-
|
|
516
|
-
// Create the target socket
|
|
394
|
+
// Create the target socket and immediately set up data piping
|
|
517
395
|
const targetSocket = plugins.net.connect(connectionOptions);
|
|
518
396
|
connectionRecord.outgoing = targetSocket;
|
|
397
|
+
connectionRecord.outgoingStartTime = Date.now();
|
|
519
398
|
|
|
520
|
-
//
|
|
521
|
-
targetSocket.once('connect', () => {
|
|
522
|
-
connectionSucceeded = true;
|
|
523
|
-
if (connectionTimeout) {
|
|
524
|
-
clearTimeout(connectionTimeout);
|
|
525
|
-
connectionTimeout = null;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
connectionRecord.outgoingStartTime = Date.now();
|
|
529
|
-
console.log(
|
|
530
|
-
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
531
|
-
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
|
|
532
|
-
);
|
|
533
|
-
|
|
534
|
-
// Setup data flow after confirmed connection
|
|
535
|
-
setupDataFlow(targetSocket, initialChunk);
|
|
536
|
-
});
|
|
537
|
-
|
|
538
|
-
// Handle connection errors early
|
|
539
|
-
targetSocket.once('error', (err) => {
|
|
540
|
-
if (!connectionSucceeded) {
|
|
541
|
-
// This is an initial connection error
|
|
542
|
-
console.log(`Failed to connect to ${targetHost}:${connectionOptions.port} for ${remoteIP}: ${err.message}`);
|
|
543
|
-
if (connectionTimeout) {
|
|
544
|
-
clearTimeout(connectionTimeout);
|
|
545
|
-
connectionTimeout = null;
|
|
546
|
-
}
|
|
547
|
-
if (outgoingTerminationReason === null) {
|
|
548
|
-
outgoingTerminationReason = 'connection_failed';
|
|
549
|
-
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
550
|
-
}
|
|
551
|
-
initiateCleanupOnce('connection_failed');
|
|
552
|
-
}
|
|
553
|
-
// Other errors will be handled by the main error handler
|
|
554
|
-
});
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Sets up the data flow between sockets after successful connection
|
|
559
|
-
*/
|
|
560
|
-
const setupDataFlow = (targetSocket: plugins.net.Socket, initialChunk?: Buffer) => {
|
|
399
|
+
// Set up the pipe immediately to ensure data flows without delay
|
|
561
400
|
if (initialChunk) {
|
|
562
401
|
socket.unshift(initialChunk);
|
|
563
402
|
}
|
|
564
403
|
|
|
565
|
-
// Set appropriate timeouts for both sockets
|
|
566
|
-
socket.setTimeout(120000);
|
|
567
|
-
targetSocket.setTimeout(120000);
|
|
568
|
-
|
|
569
|
-
// Set up the pipe in both directions
|
|
570
404
|
socket.pipe(targetSocket);
|
|
571
405
|
targetSocket.pipe(socket);
|
|
406
|
+
|
|
407
|
+
console.log(
|
|
408
|
+
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
409
|
+
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
|
|
410
|
+
);
|
|
572
411
|
|
|
573
|
-
//
|
|
412
|
+
// Add appropriate handlers for connection management
|
|
574
413
|
socket.on('error', handleError('incoming'));
|
|
575
414
|
targetSocket.on('error', handleError('outgoing'));
|
|
576
415
|
socket.on('close', handleClose('incoming'));
|
|
577
416
|
targetSocket.on('close', handleClose('outgoing'));
|
|
578
|
-
|
|
579
|
-
// Handle timeout events
|
|
580
417
|
socket.on('timeout', () => {
|
|
581
418
|
console.log(`Timeout on incoming side from ${remoteIP}`);
|
|
582
419
|
if (incomingTerminationReason === null) {
|
|
583
420
|
incomingTerminationReason = 'timeout';
|
|
584
421
|
this.incrementTerminationStat('incoming', 'timeout');
|
|
585
422
|
}
|
|
586
|
-
initiateCleanupOnce('
|
|
423
|
+
initiateCleanupOnce('timeout_incoming');
|
|
587
424
|
});
|
|
588
|
-
|
|
589
425
|
targetSocket.on('timeout', () => {
|
|
590
426
|
console.log(`Timeout on outgoing side from ${remoteIP}`);
|
|
591
427
|
if (outgoingTerminationReason === null) {
|
|
592
428
|
outgoingTerminationReason = 'timeout';
|
|
593
429
|
this.incrementTerminationStat('outgoing', 'timeout');
|
|
594
430
|
}
|
|
595
|
-
initiateCleanupOnce('
|
|
431
|
+
initiateCleanupOnce('timeout_outgoing');
|
|
596
432
|
});
|
|
597
|
-
|
|
598
|
-
socket.on('end', handleClose('incoming'));
|
|
599
|
-
targetSocket.on('end', handleClose('outgoing'));
|
|
600
433
|
|
|
601
|
-
//
|
|
602
|
-
socket.
|
|
603
|
-
|
|
434
|
+
// Set appropriate timeouts
|
|
435
|
+
socket.setTimeout(120000);
|
|
436
|
+
targetSocket.setTimeout(120000);
|
|
437
|
+
|
|
438
|
+
// Update activity for both sockets
|
|
439
|
+
socket.on('data', () => {
|
|
440
|
+
connectionRecord.lastActivity = Date.now();
|
|
604
441
|
});
|
|
605
442
|
|
|
606
|
-
targetSocket.on('data', (
|
|
607
|
-
|
|
443
|
+
targetSocket.on('data', () => {
|
|
444
|
+
connectionRecord.lastActivity = Date.now();
|
|
608
445
|
});
|
|
609
446
|
|
|
610
447
|
// Initialize a cleanup timer for max connection lifetime
|
|
@@ -623,7 +460,6 @@ export class PortProxy {
|
|
|
623
460
|
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
624
461
|
console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
|
|
625
462
|
socket.end();
|
|
626
|
-
initiateCleanupOnce('rejected');
|
|
627
463
|
return;
|
|
628
464
|
}
|
|
629
465
|
console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
|
|
@@ -652,7 +488,6 @@ export class PortProxy {
|
|
|
652
488
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
653
489
|
console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
|
|
654
490
|
socket.end();
|
|
655
|
-
initiateCleanupOnce('rejected');
|
|
656
491
|
return;
|
|
657
492
|
}
|
|
658
493
|
console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
|
|
@@ -740,7 +575,7 @@ export class PortProxy {
|
|
|
740
575
|
this.netServers.push(server);
|
|
741
576
|
}
|
|
742
577
|
|
|
743
|
-
// Log active connection count,
|
|
578
|
+
// Log active connection count, longest running durations, and run parity checks every 10 seconds.
|
|
744
579
|
this.connectionLogger = setInterval(() => {
|
|
745
580
|
if (this.isShuttingDown) return;
|
|
746
581
|
|
|
@@ -760,25 +595,23 @@ export class PortProxy {
|
|
|
760
595
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
|
761
596
|
}
|
|
762
597
|
|
|
763
|
-
// Parity check: if outgoing socket closed and incoming remains active
|
|
598
|
+
// Parity check: if outgoing socket closed and incoming remains active
|
|
764
599
|
if (record.outgoingClosedTime &&
|
|
765
600
|
!record.incoming.destroyed &&
|
|
766
601
|
!record.connectionClosed &&
|
|
767
|
-
!record.cleanupInitiated &&
|
|
768
602
|
(now - record.outgoingClosedTime > 30000)) {
|
|
769
603
|
const remoteIP = record.incoming.remoteAddress || 'unknown';
|
|
770
|
-
console.log(`Parity check
|
|
771
|
-
this.
|
|
604
|
+
console.log(`Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(now - record.outgoingClosedTime)} after outgoing closed.`);
|
|
605
|
+
this.cleanupConnection(record, 'parity_check');
|
|
772
606
|
}
|
|
773
607
|
|
|
774
|
-
// Inactivity check
|
|
608
|
+
// Inactivity check
|
|
775
609
|
const inactivityTime = now - record.lastActivity;
|
|
776
610
|
if (inactivityTime > 180000 && // 3 minutes
|
|
777
|
-
!record.connectionClosed
|
|
778
|
-
!record.cleanupInitiated) {
|
|
611
|
+
!record.connectionClosed) {
|
|
779
612
|
const remoteIP = record.incoming.remoteAddress || 'unknown';
|
|
780
|
-
console.log(`Inactivity check
|
|
781
|
-
this.
|
|
613
|
+
console.log(`Inactivity check: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
|
|
614
|
+
this.cleanupConnection(record, 'inactivity');
|
|
782
615
|
}
|
|
783
616
|
}
|
|
784
617
|
|
|
@@ -813,14 +646,14 @@ export class PortProxy {
|
|
|
813
646
|
await Promise.all(closeServerPromises);
|
|
814
647
|
console.log("All servers closed. Cleaning up active connections...");
|
|
815
648
|
|
|
816
|
-
//
|
|
649
|
+
// Clean up active connections
|
|
817
650
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
818
651
|
console.log(`Cleaning up ${connectionIds.length} active connections...`);
|
|
819
652
|
|
|
820
653
|
for (const id of connectionIds) {
|
|
821
654
|
const record = this.connectionRecords.get(id);
|
|
822
|
-
if (record && !record.connectionClosed
|
|
823
|
-
this.
|
|
655
|
+
if (record && !record.connectionClosed) {
|
|
656
|
+
this.cleanupConnection(record, 'shutdown');
|
|
824
657
|
}
|
|
825
658
|
}
|
|
826
659
|
|