@push.rocks/smartproxy 3.22.0 → 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 +117 -285
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.portproxy.ts +133 -324
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,55 +286,25 @@ export class PortProxy {
|
|
|
321
286
|
incomingTerminationReason = reason;
|
|
322
287
|
this.incrementTerminationStat('incoming', reason);
|
|
323
288
|
}
|
|
324
|
-
|
|
289
|
+
cleanupOnce();
|
|
325
290
|
};
|
|
326
291
|
|
|
327
|
-
// Set an initial timeout
|
|
328
|
-
// For chained proxies, we need to allow more time for data to flow through
|
|
329
|
-
const initialTimeoutMs = this.settings.initialDataTimeout ||
|
|
330
|
-
(this.settings.sniEnabled ? 15000 : 0); // Increased timeout for SNI, disabled for non-SNI by default
|
|
331
|
-
|
|
292
|
+
// Set an initial timeout for SNI data if needed
|
|
332
293
|
let initialTimeout: NodeJS.Timeout | null = null;
|
|
333
|
-
|
|
334
|
-
if (initialTimeoutMs > 0) {
|
|
335
|
-
console.log(`Setting initial data timeout of ${initialTimeoutMs}ms for connection from ${remoteIP}`);
|
|
294
|
+
if (this.settings.sniEnabled) {
|
|
336
295
|
initialTimeout = setTimeout(() => {
|
|
337
296
|
if (!initialDataReceived) {
|
|
338
|
-
console.log(`Initial
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
this.incrementTerminationStat('incoming', 'initial_timeout');
|
|
342
|
-
}
|
|
343
|
-
initiateCleanupOnce('initial_timeout');
|
|
297
|
+
console.log(`Initial data timeout for ${remoteIP}`);
|
|
298
|
+
socket.end();
|
|
299
|
+
cleanupOnce();
|
|
344
300
|
}
|
|
345
|
-
},
|
|
301
|
+
}, 5000);
|
|
346
302
|
} else {
|
|
347
|
-
console.log(`No initial timeout set for connection from ${remoteIP} (likely chained proxy)`);
|
|
348
|
-
// Mark as received immediately if we're not waiting for data
|
|
349
303
|
initialDataReceived = true;
|
|
350
304
|
}
|
|
351
305
|
|
|
352
306
|
socket.on('error', (err: Error) => {
|
|
353
|
-
|
|
354
|
-
? `(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`
|
|
355
|
-
: `(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`;
|
|
356
|
-
console.log(errorMessage);
|
|
357
|
-
|
|
358
|
-
// Clear the initial timeout if it exists
|
|
359
|
-
if (initialTimeout) {
|
|
360
|
-
clearTimeout(initialTimeout);
|
|
361
|
-
initialTimeout = null;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// For premature errors, we need to handle them explicitly
|
|
365
|
-
// since the standard error handlers might not be set up yet
|
|
366
|
-
if (!initialDataReceived) {
|
|
367
|
-
if (incomingTerminationReason === null) {
|
|
368
|
-
incomingTerminationReason = 'premature_error';
|
|
369
|
-
this.incrementTerminationStat('incoming', 'premature_error');
|
|
370
|
-
}
|
|
371
|
-
initiateCleanupOnce('premature_error');
|
|
372
|
-
}
|
|
307
|
+
console.log(`Incoming socket error from ${remoteIP}: ${err.message}`);
|
|
373
308
|
});
|
|
374
309
|
|
|
375
310
|
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
|
@@ -378,13 +313,9 @@ export class PortProxy {
|
|
|
378
313
|
if (code === 'ECONNRESET') {
|
|
379
314
|
reason = 'econnreset';
|
|
380
315
|
console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
|
|
381
|
-
} else if (code === 'ECONNREFUSED') {
|
|
382
|
-
reason = 'econnrefused';
|
|
383
|
-
console.log(`ECONNREFUSED on ${side} side from ${remoteIP}: ${err.message}`);
|
|
384
316
|
} else {
|
|
385
317
|
console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
|
|
386
318
|
}
|
|
387
|
-
|
|
388
319
|
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
389
320
|
incomingTerminationReason = reason;
|
|
390
321
|
this.incrementTerminationStat('incoming', reason);
|
|
@@ -392,13 +323,11 @@ export class PortProxy {
|
|
|
392
323
|
outgoingTerminationReason = reason;
|
|
393
324
|
this.incrementTerminationStat('outgoing', reason);
|
|
394
325
|
}
|
|
395
|
-
|
|
396
326
|
initiateCleanupOnce(reason);
|
|
397
327
|
};
|
|
398
328
|
|
|
399
329
|
const handleClose = (side: 'incoming' | 'outgoing') => () => {
|
|
400
330
|
console.log(`Connection closed on ${side} side from ${remoteIP}`);
|
|
401
|
-
|
|
402
331
|
if (side === 'incoming' && incomingTerminationReason === null) {
|
|
403
332
|
incomingTerminationReason = 'normal';
|
|
404
333
|
this.incrementTerminationStat('incoming', 'normal');
|
|
@@ -407,24 +336,8 @@ export class PortProxy {
|
|
|
407
336
|
this.incrementTerminationStat('outgoing', 'normal');
|
|
408
337
|
// Record the time when outgoing socket closed.
|
|
409
338
|
connectionRecord.outgoingClosedTime = Date.now();
|
|
410
|
-
|
|
411
|
-
// If incoming is still active but outgoing closed, set a shorter timeout
|
|
412
|
-
if (!connectionRecord.incoming.destroyed) {
|
|
413
|
-
console.log(`Outgoing socket closed but incoming still active for ${remoteIP}. Setting cleanup timeout.`);
|
|
414
|
-
setTimeout(() => {
|
|
415
|
-
if (!connectionRecord.connectionClosed && !connectionRecord.incoming.destroyed) {
|
|
416
|
-
console.log(`Incoming socket still active ${Date.now() - connectionRecord.outgoingClosedTime!}ms after outgoing closed for ${remoteIP}. Cleaning up.`);
|
|
417
|
-
initiateCleanupOnce('outgoing_closed_timeout');
|
|
418
|
-
}
|
|
419
|
-
}, 10000); // 10 second timeout instead of waiting for the next parity check
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// If both sides are closed/destroyed, clean up
|
|
424
|
-
if ((side === 'incoming' && connectionRecord.outgoing?.destroyed) ||
|
|
425
|
-
(side === 'outgoing' && connectionRecord.incoming.destroyed)) {
|
|
426
|
-
initiateCleanupOnce('both_closed');
|
|
427
339
|
}
|
|
340
|
+
initiateCleanupOnce('closed_' + side);
|
|
428
341
|
};
|
|
429
342
|
|
|
430
343
|
/**
|
|
@@ -438,6 +351,7 @@ export class PortProxy {
|
|
|
438
351
|
// Clear the initial timeout since we've received data
|
|
439
352
|
if (initialTimeout) {
|
|
440
353
|
clearTimeout(initialTimeout);
|
|
354
|
+
initialTimeout = null;
|
|
441
355
|
}
|
|
442
356
|
|
|
443
357
|
// If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
|
|
@@ -447,35 +361,25 @@ export class PortProxy {
|
|
|
447
361
|
config.domains.some(d => plugins.minimatch(serverName, d))
|
|
448
362
|
) : undefined);
|
|
449
363
|
|
|
450
|
-
//
|
|
451
|
-
// In a chained proxy, relax IP validation unless explicitly configured
|
|
452
|
-
// If this is the first proxy in the chain, normal validation applies
|
|
364
|
+
// IP validation is skipped if allowedIPs is empty
|
|
453
365
|
if (domainConfig) {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
]
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
} else {
|
|
468
|
-
console.log(`Domain config for ${domainConfig.domains.join(', ')} has empty allowedIPs, skipping IP validation`);
|
|
366
|
+
const effectiveAllowedIPs: string[] = [
|
|
367
|
+
...domainConfig.allowedIPs,
|
|
368
|
+
...(this.settings.defaultAllowedIPs || [])
|
|
369
|
+
];
|
|
370
|
+
const effectiveBlockedIPs: string[] = [
|
|
371
|
+
...(domainConfig.blockedIPs || []),
|
|
372
|
+
...(this.settings.defaultBlockedIPs || [])
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
// Skip IP validation if allowedIPs is empty
|
|
376
|
+
if (domainConfig.allowedIPs.length > 0 && !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
377
|
+
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`);
|
|
469
378
|
}
|
|
470
379
|
} else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
471
|
-
// No domain config but has default IP restrictions
|
|
472
380
|
if (!isGlobIPAllowed(remoteIP, this.settings.defaultAllowedIPs, this.settings.defaultBlockedIPs || [])) {
|
|
473
381
|
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed by default allowed list`);
|
|
474
382
|
}
|
|
475
|
-
} else {
|
|
476
|
-
// No domain config and no default allowed IPs
|
|
477
|
-
// In a chained proxy setup, we'll allow this connection
|
|
478
|
-
console.log(`No specific IP restrictions found for ${remoteIP}. Allowing connection in potential chained proxy setup.`);
|
|
479
383
|
}
|
|
480
384
|
|
|
481
385
|
const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
|
|
@@ -487,116 +391,57 @@ export class PortProxy {
|
|
|
487
391
|
connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
|
|
488
392
|
}
|
|
489
393
|
|
|
490
|
-
//
|
|
491
|
-
let connectionTimeout: NodeJS.Timeout | null = null;
|
|
492
|
-
let connectionSucceeded = false;
|
|
493
|
-
|
|
494
|
-
// Set connection timeout - longer for chained proxies
|
|
495
|
-
connectionTimeout = setTimeout(() => {
|
|
496
|
-
if (!connectionSucceeded) {
|
|
497
|
-
console.log(`Connection timeout connecting to ${targetHost}:${connectionOptions.port} for ${remoteIP}`);
|
|
498
|
-
if (outgoingTerminationReason === null) {
|
|
499
|
-
outgoingTerminationReason = 'connection_timeout';
|
|
500
|
-
this.incrementTerminationStat('outgoing', 'connection_timeout');
|
|
501
|
-
}
|
|
502
|
-
initiateCleanupOnce('connection_timeout');
|
|
503
|
-
}
|
|
504
|
-
}, 10000); // Increased from 5s to 10s to accommodate chained proxies
|
|
505
|
-
|
|
506
|
-
console.log(`Attempting to connect to ${targetHost}:${connectionOptions.port} for client ${remoteIP}...`);
|
|
507
|
-
|
|
508
|
-
// Create the target socket
|
|
394
|
+
// Create the target socket and immediately set up data piping
|
|
509
395
|
const targetSocket = plugins.net.connect(connectionOptions);
|
|
510
396
|
connectionRecord.outgoing = targetSocket;
|
|
397
|
+
connectionRecord.outgoingStartTime = Date.now();
|
|
511
398
|
|
|
512
|
-
//
|
|
513
|
-
targetSocket.once('connect', () => {
|
|
514
|
-
connectionSucceeded = true;
|
|
515
|
-
if (connectionTimeout) {
|
|
516
|
-
clearTimeout(connectionTimeout);
|
|
517
|
-
connectionTimeout = null;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
connectionRecord.outgoingStartTime = Date.now();
|
|
521
|
-
console.log(
|
|
522
|
-
`Connection established: ${remoteIP} -> ${targetHost}:${connectionOptions.port}` +
|
|
523
|
-
`${serverName ? ` (SNI: ${serverName})` : forcedDomain ? ` (Port-based for domain: ${forcedDomain.domains.join(', ')})` : ''}`
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
// Setup data flow after confirmed connection
|
|
527
|
-
setupDataFlow(targetSocket, initialChunk);
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// Handle connection errors early
|
|
531
|
-
targetSocket.once('error', (err) => {
|
|
532
|
-
if (!connectionSucceeded) {
|
|
533
|
-
// This is an initial connection error
|
|
534
|
-
console.log(`Failed to connect to ${targetHost}:${connectionOptions.port} for ${remoteIP}: ${err.message}`);
|
|
535
|
-
if (connectionTimeout) {
|
|
536
|
-
clearTimeout(connectionTimeout);
|
|
537
|
-
connectionTimeout = null;
|
|
538
|
-
}
|
|
539
|
-
if (outgoingTerminationReason === null) {
|
|
540
|
-
outgoingTerminationReason = 'connection_failed';
|
|
541
|
-
this.incrementTerminationStat('outgoing', 'connection_failed');
|
|
542
|
-
}
|
|
543
|
-
initiateCleanupOnce('connection_failed');
|
|
544
|
-
}
|
|
545
|
-
// Other errors will be handled by the main error handler
|
|
546
|
-
});
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
/**
|
|
550
|
-
* Sets up the data flow between sockets after successful connection
|
|
551
|
-
*/
|
|
552
|
-
const setupDataFlow = (targetSocket: plugins.net.Socket, initialChunk?: Buffer) => {
|
|
399
|
+
// Set up the pipe immediately to ensure data flows without delay
|
|
553
400
|
if (initialChunk) {
|
|
554
401
|
socket.unshift(initialChunk);
|
|
555
402
|
}
|
|
556
403
|
|
|
557
|
-
// Set appropriate timeouts for both sockets
|
|
558
|
-
socket.setTimeout(120000);
|
|
559
|
-
targetSocket.setTimeout(120000);
|
|
560
|
-
|
|
561
|
-
// Set up the pipe in both directions
|
|
562
404
|
socket.pipe(targetSocket);
|
|
563
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
|
+
);
|
|
564
411
|
|
|
565
|
-
//
|
|
412
|
+
// Add appropriate handlers for connection management
|
|
566
413
|
socket.on('error', handleError('incoming'));
|
|
567
414
|
targetSocket.on('error', handleError('outgoing'));
|
|
568
415
|
socket.on('close', handleClose('incoming'));
|
|
569
416
|
targetSocket.on('close', handleClose('outgoing'));
|
|
570
|
-
|
|
571
|
-
// Handle timeout events
|
|
572
417
|
socket.on('timeout', () => {
|
|
573
418
|
console.log(`Timeout on incoming side from ${remoteIP}`);
|
|
574
419
|
if (incomingTerminationReason === null) {
|
|
575
420
|
incomingTerminationReason = 'timeout';
|
|
576
421
|
this.incrementTerminationStat('incoming', 'timeout');
|
|
577
422
|
}
|
|
578
|
-
initiateCleanupOnce('
|
|
423
|
+
initiateCleanupOnce('timeout_incoming');
|
|
579
424
|
});
|
|
580
|
-
|
|
581
425
|
targetSocket.on('timeout', () => {
|
|
582
426
|
console.log(`Timeout on outgoing side from ${remoteIP}`);
|
|
583
427
|
if (outgoingTerminationReason === null) {
|
|
584
428
|
outgoingTerminationReason = 'timeout';
|
|
585
429
|
this.incrementTerminationStat('outgoing', 'timeout');
|
|
586
430
|
}
|
|
587
|
-
initiateCleanupOnce('
|
|
431
|
+
initiateCleanupOnce('timeout_outgoing');
|
|
588
432
|
});
|
|
589
|
-
|
|
590
|
-
socket.on('end', handleClose('incoming'));
|
|
591
|
-
targetSocket.on('end', handleClose('outgoing'));
|
|
592
433
|
|
|
593
|
-
//
|
|
594
|
-
socket.
|
|
595
|
-
|
|
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();
|
|
596
441
|
});
|
|
597
442
|
|
|
598
|
-
targetSocket.on('data', (
|
|
599
|
-
|
|
443
|
+
targetSocket.on('data', () => {
|
|
444
|
+
connectionRecord.lastActivity = Date.now();
|
|
600
445
|
});
|
|
601
446
|
|
|
602
447
|
// Initialize a cleanup timer for max connection lifetime
|
|
@@ -615,7 +460,6 @@ export class PortProxy {
|
|
|
615
460
|
if (this.settings.defaultAllowedIPs && !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
616
461
|
console.log(`Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`);
|
|
617
462
|
socket.end();
|
|
618
|
-
initiateCleanupOnce('rejected');
|
|
619
463
|
return;
|
|
620
464
|
}
|
|
621
465
|
console.log(`Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`);
|
|
@@ -644,7 +488,6 @@ export class PortProxy {
|
|
|
644
488
|
if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
|
|
645
489
|
console.log(`Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(', ')} on port ${localPort}.`);
|
|
646
490
|
socket.end();
|
|
647
|
-
initiateCleanupOnce('rejected');
|
|
648
491
|
return;
|
|
649
492
|
}
|
|
650
493
|
console.log(`Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(', ')}.`);
|
|
@@ -657,14 +500,8 @@ export class PortProxy {
|
|
|
657
500
|
|
|
658
501
|
// --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
|
|
659
502
|
if (this.settings.sniEnabled) {
|
|
660
|
-
// If using SNI, we need to wait for data to establish the connection
|
|
661
|
-
if (initialDataReceived) {
|
|
662
|
-
console.log(`Initial data already marked as received for ${remoteIP}, but SNI is enabled. This is unexpected.`);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
503
|
initialDataReceived = false;
|
|
666
|
-
|
|
667
|
-
console.log(`Waiting for TLS ClientHello from ${remoteIP} to extract SNI...`);
|
|
504
|
+
|
|
668
505
|
socket.once('data', (chunk: Buffer) => {
|
|
669
506
|
if (initialTimeout) {
|
|
670
507
|
clearTimeout(initialTimeout);
|
|
@@ -672,23 +509,10 @@ export class PortProxy {
|
|
|
672
509
|
}
|
|
673
510
|
|
|
674
511
|
initialDataReceived = true;
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
let serverName = '';
|
|
678
|
-
try {
|
|
679
|
-
// Only try to extract SNI if the chunk looks like a TLS ClientHello
|
|
680
|
-
if (chunk.length > 5 && chunk.readUInt8(0) === 22) {
|
|
681
|
-
serverName = extractSNI(chunk) || '';
|
|
682
|
-
console.log(`Extracted SNI: "${serverName}" from connection ${remoteIP}`);
|
|
683
|
-
} else {
|
|
684
|
-
console.log(`Data from ${remoteIP} doesn't appear to be a TLS ClientHello. First byte: ${chunk.length > 0 ? chunk.readUInt8(0) : 'N/A'}`);
|
|
685
|
-
}
|
|
686
|
-
} catch (err) {
|
|
687
|
-
console.log(`Error extracting SNI from chunk: ${err}. Proceeding without SNI.`);
|
|
688
|
-
}
|
|
689
|
-
|
|
512
|
+
const serverName = extractSNI(chunk) || '';
|
|
690
513
|
// Lock the connection to the negotiated SNI.
|
|
691
514
|
connectionRecord.lockedDomain = serverName;
|
|
515
|
+
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
|
|
692
516
|
|
|
693
517
|
// Delay adding the renegotiation listener until the next tick,
|
|
694
518
|
// so the initial ClientHello is not reprocessed.
|
|
@@ -714,23 +538,10 @@ export class PortProxy {
|
|
|
714
538
|
setupConnection(serverName, chunk);
|
|
715
539
|
});
|
|
716
540
|
} else {
|
|
717
|
-
// Non-SNI mode: we can proceed immediately without waiting for data
|
|
718
|
-
if (initialTimeout) {
|
|
719
|
-
clearTimeout(initialTimeout);
|
|
720
|
-
initialTimeout = null;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
541
|
initialDataReceived = true;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
// Check IP restrictions only if explicitly configured
|
|
727
|
-
if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
|
|
728
|
-
if (!isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
729
|
-
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
|
730
|
-
}
|
|
542
|
+
if (!this.settings.defaultAllowedIPs || this.settings.defaultAllowedIPs.length === 0 || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
|
543
|
+
return rejectIncomingConnection('rejected', `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
|
731
544
|
}
|
|
732
|
-
|
|
733
|
-
// Proceed with connection setup
|
|
734
545
|
setupConnection('');
|
|
735
546
|
}
|
|
736
547
|
};
|
|
@@ -764,7 +575,7 @@ export class PortProxy {
|
|
|
764
575
|
this.netServers.push(server);
|
|
765
576
|
}
|
|
766
577
|
|
|
767
|
-
// Log active connection count,
|
|
578
|
+
// Log active connection count, longest running durations, and run parity checks every 10 seconds.
|
|
768
579
|
this.connectionLogger = setInterval(() => {
|
|
769
580
|
if (this.isShuttingDown) return;
|
|
770
581
|
|
|
@@ -784,25 +595,23 @@ export class PortProxy {
|
|
|
784
595
|
maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
|
|
785
596
|
}
|
|
786
597
|
|
|
787
|
-
// Parity check: if outgoing socket closed and incoming remains active
|
|
598
|
+
// Parity check: if outgoing socket closed and incoming remains active
|
|
788
599
|
if (record.outgoingClosedTime &&
|
|
789
600
|
!record.incoming.destroyed &&
|
|
790
601
|
!record.connectionClosed &&
|
|
791
|
-
!record.cleanupInitiated &&
|
|
792
602
|
(now - record.outgoingClosedTime > 30000)) {
|
|
793
603
|
const remoteIP = record.incoming.remoteAddress || 'unknown';
|
|
794
|
-
console.log(`Parity check
|
|
795
|
-
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');
|
|
796
606
|
}
|
|
797
607
|
|
|
798
|
-
// Inactivity check
|
|
608
|
+
// Inactivity check
|
|
799
609
|
const inactivityTime = now - record.lastActivity;
|
|
800
610
|
if (inactivityTime > 180000 && // 3 minutes
|
|
801
|
-
!record.connectionClosed
|
|
802
|
-
!record.cleanupInitiated) {
|
|
611
|
+
!record.connectionClosed) {
|
|
803
612
|
const remoteIP = record.incoming.remoteAddress || 'unknown';
|
|
804
|
-
console.log(`Inactivity check
|
|
805
|
-
this.
|
|
613
|
+
console.log(`Inactivity check: No activity on connection from ${remoteIP} for ${plugins.prettyMs(inactivityTime)}.`);
|
|
614
|
+
this.cleanupConnection(record, 'inactivity');
|
|
806
615
|
}
|
|
807
616
|
}
|
|
808
617
|
|
|
@@ -837,14 +646,14 @@ export class PortProxy {
|
|
|
837
646
|
await Promise.all(closeServerPromises);
|
|
838
647
|
console.log("All servers closed. Cleaning up active connections...");
|
|
839
648
|
|
|
840
|
-
//
|
|
649
|
+
// Clean up active connections
|
|
841
650
|
const connectionIds = [...this.connectionRecords.keys()];
|
|
842
651
|
console.log(`Cleaning up ${connectionIds.length} active connections...`);
|
|
843
652
|
|
|
844
653
|
for (const id of connectionIds) {
|
|
845
654
|
const record = this.connectionRecords.get(id);
|
|
846
|
-
if (record && !record.connectionClosed
|
|
847
|
-
this.
|
|
655
|
+
if (record && !record.connectionClosed) {
|
|
656
|
+
this.cleanupConnection(record, 'shutdown');
|
|
848
657
|
}
|
|
849
658
|
}
|
|
850
659
|
|