@push.rocks/smartproxy 3.41.8 → 4.0.0
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 +2 -2
- package/dist_ts/classes.pp.acmemanager.d.ts +34 -0
- package/dist_ts/classes.pp.acmemanager.js +123 -0
- package/dist_ts/classes.pp.connectionhandler.d.ts +39 -0
- package/dist_ts/classes.pp.connectionhandler.js +693 -0
- package/dist_ts/classes.pp.connectionmanager.d.ts +78 -0
- package/dist_ts/classes.pp.connectionmanager.js +378 -0
- package/dist_ts/classes.pp.domainconfigmanager.d.ts +55 -0
- package/dist_ts/classes.pp.domainconfigmanager.js +103 -0
- package/dist_ts/classes.pp.interfaces.d.ts +109 -0
- package/dist_ts/classes.pp.interfaces.js +2 -0
- package/dist_ts/classes.pp.networkproxybridge.d.ts +43 -0
- package/dist_ts/classes.pp.networkproxybridge.js +211 -0
- package/dist_ts/classes.pp.portproxy.d.ts +48 -0
- package/dist_ts/classes.pp.portproxy.js +268 -0
- package/dist_ts/classes.pp.portrangemanager.d.ts +56 -0
- package/dist_ts/classes.pp.portrangemanager.js +179 -0
- package/dist_ts/classes.pp.securitymanager.d.ts +47 -0
- package/dist_ts/classes.pp.securitymanager.js +126 -0
- package/dist_ts/classes.pp.snihandler.d.ts +160 -0
- package/dist_ts/classes.pp.snihandler.js +1073 -0
- package/dist_ts/classes.pp.timeoutmanager.d.ts +47 -0
- package/dist_ts/classes.pp.timeoutmanager.js +154 -0
- package/dist_ts/classes.pp.tlsmanager.d.ts +57 -0
- package/dist_ts/classes.pp.tlsmanager.js +132 -0
- package/dist_ts/index.d.ts +2 -2
- package/dist_ts/index.js +3 -3
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.pp.acmemanager.ts +149 -0
- package/ts/classes.pp.connectionhandler.ts +982 -0
- package/ts/classes.pp.connectionmanager.ts +446 -0
- package/ts/classes.pp.domainconfigmanager.ts +123 -0
- package/ts/classes.pp.interfaces.ts +136 -0
- package/ts/classes.pp.networkproxybridge.ts +258 -0
- package/ts/classes.pp.portproxy.ts +344 -0
- package/ts/classes.pp.portrangemanager.ts +214 -0
- package/ts/classes.pp.securitymanager.ts +147 -0
- package/ts/{classes.snihandler.ts → classes.pp.snihandler.ts} +2 -169
- package/ts/classes.pp.timeoutmanager.ts +190 -0
- package/ts/classes.pp.tlsmanager.ts +206 -0
- package/ts/index.ts +2 -2
- package/ts/classes.portproxy.ts +0 -2503
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IConnectionRecord, IPortProxySettings } from './classes.pp.interfaces.js';
|
|
3
|
+
import { SecurityManager } from './classes.pp.securitymanager.js';
|
|
4
|
+
import { TimeoutManager } from './classes.pp.timeoutmanager.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manages connection lifecycle, tracking, and cleanup
|
|
8
|
+
*/
|
|
9
|
+
export class ConnectionManager {
|
|
10
|
+
private connectionRecords: Map<string, IConnectionRecord> = new Map();
|
|
11
|
+
private terminationStats: {
|
|
12
|
+
incoming: Record<string, number>;
|
|
13
|
+
outgoing: Record<string, number>;
|
|
14
|
+
} = { incoming: {}, outgoing: {} };
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private settings: IPortProxySettings,
|
|
18
|
+
private securityManager: SecurityManager,
|
|
19
|
+
private timeoutManager: TimeoutManager
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Generate a unique connection ID
|
|
24
|
+
*/
|
|
25
|
+
public generateConnectionId(): string {
|
|
26
|
+
return Math.random().toString(36).substring(2, 15) +
|
|
27
|
+
Math.random().toString(36).substring(2, 15);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create and track a new connection
|
|
32
|
+
*/
|
|
33
|
+
public createConnection(socket: plugins.net.Socket): IConnectionRecord {
|
|
34
|
+
const connectionId = this.generateConnectionId();
|
|
35
|
+
const remoteIP = socket.remoteAddress || '';
|
|
36
|
+
const localPort = socket.localPort || 0;
|
|
37
|
+
|
|
38
|
+
const record: IConnectionRecord = {
|
|
39
|
+
id: connectionId,
|
|
40
|
+
incoming: socket,
|
|
41
|
+
outgoing: null,
|
|
42
|
+
incomingStartTime: Date.now(),
|
|
43
|
+
lastActivity: Date.now(),
|
|
44
|
+
connectionClosed: false,
|
|
45
|
+
pendingData: [],
|
|
46
|
+
pendingDataSize: 0,
|
|
47
|
+
bytesReceived: 0,
|
|
48
|
+
bytesSent: 0,
|
|
49
|
+
remoteIP,
|
|
50
|
+
localPort,
|
|
51
|
+
isTLS: false,
|
|
52
|
+
tlsHandshakeComplete: false,
|
|
53
|
+
hasReceivedInitialData: false,
|
|
54
|
+
hasKeepAlive: false,
|
|
55
|
+
incomingTerminationReason: null,
|
|
56
|
+
outgoingTerminationReason: null,
|
|
57
|
+
usingNetworkProxy: false,
|
|
58
|
+
isBrowserConnection: false,
|
|
59
|
+
domainSwitches: 0
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
this.trackConnection(connectionId, record);
|
|
63
|
+
return record;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Track an existing connection
|
|
68
|
+
*/
|
|
69
|
+
public trackConnection(connectionId: string, record: IConnectionRecord): void {
|
|
70
|
+
this.connectionRecords.set(connectionId, record);
|
|
71
|
+
this.securityManager.trackConnectionByIP(record.remoteIP, connectionId);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get a connection by ID
|
|
76
|
+
*/
|
|
77
|
+
public getConnection(connectionId: string): IConnectionRecord | undefined {
|
|
78
|
+
return this.connectionRecords.get(connectionId);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get all active connections
|
|
83
|
+
*/
|
|
84
|
+
public getConnections(): Map<string, IConnectionRecord> {
|
|
85
|
+
return this.connectionRecords;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get count of active connections
|
|
90
|
+
*/
|
|
91
|
+
public getConnectionCount(): number {
|
|
92
|
+
return this.connectionRecords.size;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Initiates cleanup once for a connection
|
|
97
|
+
*/
|
|
98
|
+
public initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
|
|
99
|
+
if (this.settings.enableDetailedLogging) {
|
|
100
|
+
console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
record.incomingTerminationReason === null ||
|
|
105
|
+
record.incomingTerminationReason === undefined
|
|
106
|
+
) {
|
|
107
|
+
record.incomingTerminationReason = reason;
|
|
108
|
+
this.incrementTerminationStat('incoming', reason);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.cleanupConnection(record, reason);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Clean up a connection record
|
|
116
|
+
*/
|
|
117
|
+
public cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
|
|
118
|
+
if (!record.connectionClosed) {
|
|
119
|
+
record.connectionClosed = true;
|
|
120
|
+
|
|
121
|
+
// Track connection termination
|
|
122
|
+
this.securityManager.removeConnectionByIP(record.remoteIP, record.id);
|
|
123
|
+
|
|
124
|
+
if (record.cleanupTimer) {
|
|
125
|
+
clearTimeout(record.cleanupTimer);
|
|
126
|
+
record.cleanupTimer = undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Detailed logging data
|
|
130
|
+
const duration = Date.now() - record.incomingStartTime;
|
|
131
|
+
const bytesReceived = record.bytesReceived;
|
|
132
|
+
const bytesSent = record.bytesSent;
|
|
133
|
+
|
|
134
|
+
// Remove all data handlers to make sure we clean up properly
|
|
135
|
+
if (record.incoming) {
|
|
136
|
+
try {
|
|
137
|
+
// Remove our safe data handler
|
|
138
|
+
record.incoming.removeAllListeners('data');
|
|
139
|
+
// Reset the handler references
|
|
140
|
+
record.renegotiationHandler = undefined;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.log(`[${record.id}] Error removing data handlers: ${err}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle incoming socket
|
|
147
|
+
this.cleanupSocket(record, 'incoming', record.incoming);
|
|
148
|
+
|
|
149
|
+
// Handle outgoing socket
|
|
150
|
+
if (record.outgoing) {
|
|
151
|
+
this.cleanupSocket(record, 'outgoing', record.outgoing);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Clear pendingData to avoid memory leaks
|
|
155
|
+
record.pendingData = [];
|
|
156
|
+
record.pendingDataSize = 0;
|
|
157
|
+
|
|
158
|
+
// Remove the record from the tracking map
|
|
159
|
+
this.connectionRecords.delete(record.id);
|
|
160
|
+
|
|
161
|
+
// Log connection details
|
|
162
|
+
if (this.settings.enableDetailedLogging) {
|
|
163
|
+
console.log(
|
|
164
|
+
`[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
|
|
165
|
+
` Duration: ${plugins.prettyMs(duration)}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
|
|
166
|
+
`TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${record.hasKeepAlive ? 'Yes' : 'No'}` +
|
|
167
|
+
`${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
|
|
168
|
+
`${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
|
|
169
|
+
);
|
|
170
|
+
} else {
|
|
171
|
+
console.log(
|
|
172
|
+
`[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Helper method to clean up a socket
|
|
180
|
+
*/
|
|
181
|
+
private cleanupSocket(record: IConnectionRecord, side: 'incoming' | 'outgoing', socket: plugins.net.Socket): void {
|
|
182
|
+
try {
|
|
183
|
+
if (!socket.destroyed) {
|
|
184
|
+
// Try graceful shutdown first, then force destroy after a short timeout
|
|
185
|
+
socket.end();
|
|
186
|
+
const socketTimeout = setTimeout(() => {
|
|
187
|
+
try {
|
|
188
|
+
if (!socket.destroyed) {
|
|
189
|
+
socket.destroy();
|
|
190
|
+
}
|
|
191
|
+
} catch (err) {
|
|
192
|
+
console.log(`[${record.id}] Error destroying ${side} socket: ${err}`);
|
|
193
|
+
}
|
|
194
|
+
}, 1000);
|
|
195
|
+
|
|
196
|
+
// Ensure the timeout doesn't block Node from exiting
|
|
197
|
+
if (socketTimeout.unref) {
|
|
198
|
+
socketTimeout.unref();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.log(`[${record.id}] Error closing ${side} socket: ${err}`);
|
|
203
|
+
try {
|
|
204
|
+
if (!socket.destroyed) {
|
|
205
|
+
socket.destroy();
|
|
206
|
+
}
|
|
207
|
+
} catch (destroyErr) {
|
|
208
|
+
console.log(`[${record.id}] Error destroying ${side} socket: ${destroyErr}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Creates a generic error handler for incoming or outgoing sockets
|
|
215
|
+
*/
|
|
216
|
+
public handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
|
217
|
+
return (err: Error) => {
|
|
218
|
+
const code = (err as any).code;
|
|
219
|
+
let reason = 'error';
|
|
220
|
+
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const connectionDuration = now - record.incomingStartTime;
|
|
223
|
+
const lastActivityAge = now - record.lastActivity;
|
|
224
|
+
|
|
225
|
+
if (code === 'ECONNRESET') {
|
|
226
|
+
reason = 'econnreset';
|
|
227
|
+
console.log(
|
|
228
|
+
`[${record.id}] ECONNRESET on ${side} side from ${record.remoteIP}: ${err.message}. ` +
|
|
229
|
+
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`
|
|
230
|
+
);
|
|
231
|
+
} else if (code === 'ETIMEDOUT') {
|
|
232
|
+
reason = 'etimedout';
|
|
233
|
+
console.log(
|
|
234
|
+
`[${record.id}] ETIMEDOUT on ${side} side from ${record.remoteIP}: ${err.message}. ` +
|
|
235
|
+
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
console.log(
|
|
239
|
+
`[${record.id}] Error on ${side} side from ${record.remoteIP}: ${err.message}. ` +
|
|
240
|
+
`Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(lastActivityAge)} ago`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (side === 'incoming' && record.incomingTerminationReason === null) {
|
|
245
|
+
record.incomingTerminationReason = reason;
|
|
246
|
+
this.incrementTerminationStat('incoming', reason);
|
|
247
|
+
} else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
|
|
248
|
+
record.outgoingTerminationReason = reason;
|
|
249
|
+
this.incrementTerminationStat('outgoing', reason);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.initiateCleanupOnce(record, reason);
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Creates a generic close handler for incoming or outgoing sockets
|
|
258
|
+
*/
|
|
259
|
+
public handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
|
|
260
|
+
return () => {
|
|
261
|
+
if (this.settings.enableDetailedLogging) {
|
|
262
|
+
console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (side === 'incoming' && record.incomingTerminationReason === null) {
|
|
266
|
+
record.incomingTerminationReason = 'normal';
|
|
267
|
+
this.incrementTerminationStat('incoming', 'normal');
|
|
268
|
+
} else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
|
|
269
|
+
record.outgoingTerminationReason = 'normal';
|
|
270
|
+
this.incrementTerminationStat('outgoing', 'normal');
|
|
271
|
+
// Record the time when outgoing socket closed.
|
|
272
|
+
record.outgoingClosedTime = Date.now();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this.initiateCleanupOnce(record, 'closed_' + side);
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Increment termination statistics
|
|
281
|
+
*/
|
|
282
|
+
public incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
|
|
283
|
+
this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Get termination statistics
|
|
288
|
+
*/
|
|
289
|
+
public getTerminationStats(): { incoming: Record<string, number>; outgoing: Record<string, number> } {
|
|
290
|
+
return this.terminationStats;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check for stalled/inactive connections
|
|
295
|
+
*/
|
|
296
|
+
public performInactivityCheck(): void {
|
|
297
|
+
const now = Date.now();
|
|
298
|
+
const connectionIds = [...this.connectionRecords.keys()];
|
|
299
|
+
|
|
300
|
+
for (const id of connectionIds) {
|
|
301
|
+
const record = this.connectionRecords.get(id);
|
|
302
|
+
if (!record) continue;
|
|
303
|
+
|
|
304
|
+
// Skip inactivity check if disabled or for immortal keep-alive connections
|
|
305
|
+
if (
|
|
306
|
+
this.settings.disableInactivityCheck ||
|
|
307
|
+
(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
|
|
308
|
+
) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const inactivityTime = now - record.lastActivity;
|
|
313
|
+
|
|
314
|
+
// Use extended timeout for extended-treatment keep-alive connections
|
|
315
|
+
let effectiveTimeout = this.settings.inactivityTimeout!;
|
|
316
|
+
if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
|
|
317
|
+
const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
|
|
318
|
+
effectiveTimeout = effectiveTimeout * multiplier;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
|
|
322
|
+
// For keep-alive connections, issue a warning first
|
|
323
|
+
if (record.hasKeepAlive && !record.inactivityWarningIssued) {
|
|
324
|
+
console.log(
|
|
325
|
+
`[${id}] Warning: Keep-alive connection from ${record.remoteIP} inactive for ${
|
|
326
|
+
plugins.prettyMs(inactivityTime)
|
|
327
|
+
}. Will close in 10 minutes if no activity.`
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
// Set warning flag and add grace period
|
|
331
|
+
record.inactivityWarningIssued = true;
|
|
332
|
+
record.lastActivity = now - (effectiveTimeout - 600000);
|
|
333
|
+
|
|
334
|
+
// Try to stimulate activity with a probe packet
|
|
335
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
336
|
+
try {
|
|
337
|
+
record.outgoing.write(Buffer.alloc(0));
|
|
338
|
+
|
|
339
|
+
if (this.settings.enableDetailedLogging) {
|
|
340
|
+
console.log(`[${id}] Sent probe packet to test keep-alive connection`);
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.log(`[${id}] Error sending probe packet: ${err}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
// For non-keep-alive or after warning, close the connection
|
|
348
|
+
console.log(
|
|
349
|
+
`[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
|
|
350
|
+
`for ${plugins.prettyMs(inactivityTime)}.` +
|
|
351
|
+
(record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
|
|
352
|
+
);
|
|
353
|
+
this.cleanupConnection(record, 'inactivity');
|
|
354
|
+
}
|
|
355
|
+
} else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
|
|
356
|
+
// If activity detected after warning, clear the warning
|
|
357
|
+
if (this.settings.enableDetailedLogging) {
|
|
358
|
+
console.log(
|
|
359
|
+
`[${id}] Connection activity detected after inactivity warning, resetting warning`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
record.inactivityWarningIssued = false;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Parity check: if outgoing socket closed and incoming remains active
|
|
366
|
+
if (
|
|
367
|
+
record.outgoingClosedTime &&
|
|
368
|
+
!record.incoming.destroyed &&
|
|
369
|
+
!record.connectionClosed &&
|
|
370
|
+
now - record.outgoingClosedTime > 120000
|
|
371
|
+
) {
|
|
372
|
+
console.log(
|
|
373
|
+
`[${id}] Parity check: Incoming socket for ${record.remoteIP} still active ${
|
|
374
|
+
plugins.prettyMs(now - record.outgoingClosedTime)
|
|
375
|
+
} after outgoing closed.`
|
|
376
|
+
);
|
|
377
|
+
this.cleanupConnection(record, 'parity_check');
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Clear all connections (for shutdown)
|
|
384
|
+
*/
|
|
385
|
+
public clearConnections(): void {
|
|
386
|
+
// Create a copy of the keys to avoid modification during iteration
|
|
387
|
+
const connectionIds = [...this.connectionRecords.keys()];
|
|
388
|
+
|
|
389
|
+
// First pass: End all connections gracefully
|
|
390
|
+
for (const id of connectionIds) {
|
|
391
|
+
const record = this.connectionRecords.get(id);
|
|
392
|
+
if (record) {
|
|
393
|
+
try {
|
|
394
|
+
// Clear any timers
|
|
395
|
+
if (record.cleanupTimer) {
|
|
396
|
+
clearTimeout(record.cleanupTimer);
|
|
397
|
+
record.cleanupTimer = undefined;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// End sockets gracefully
|
|
401
|
+
if (record.incoming && !record.incoming.destroyed) {
|
|
402
|
+
record.incoming.end();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (record.outgoing && !record.outgoing.destroyed) {
|
|
406
|
+
record.outgoing.end();
|
|
407
|
+
}
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.log(`Error during graceful connection end for ${id}: ${err}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Short delay to allow graceful ends to process
|
|
415
|
+
setTimeout(() => {
|
|
416
|
+
// Second pass: Force destroy everything
|
|
417
|
+
for (const id of connectionIds) {
|
|
418
|
+
const record = this.connectionRecords.get(id);
|
|
419
|
+
if (record) {
|
|
420
|
+
try {
|
|
421
|
+
// Remove all listeners to prevent memory leaks
|
|
422
|
+
if (record.incoming) {
|
|
423
|
+
record.incoming.removeAllListeners();
|
|
424
|
+
if (!record.incoming.destroyed) {
|
|
425
|
+
record.incoming.destroy();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (record.outgoing) {
|
|
430
|
+
record.outgoing.removeAllListeners();
|
|
431
|
+
if (!record.outgoing.destroyed) {
|
|
432
|
+
record.outgoing.destroy();
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.log(`Error during forced connection destruction for ${id}: ${err}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Clear all maps
|
|
442
|
+
this.connectionRecords.clear();
|
|
443
|
+
this.terminationStats = { incoming: {}, outgoing: {} };
|
|
444
|
+
}, 100);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { IDomainConfig, IPortProxySettings } from './classes.pp.interfaces.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Manages domain configurations and target selection
|
|
6
|
+
*/
|
|
7
|
+
export class DomainConfigManager {
|
|
8
|
+
// Track round-robin indices for domain configs
|
|
9
|
+
private domainTargetIndices: Map<IDomainConfig, number> = new Map();
|
|
10
|
+
|
|
11
|
+
constructor(private settings: IPortProxySettings) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Updates the domain configurations
|
|
15
|
+
*/
|
|
16
|
+
public updateDomainConfigs(newDomainConfigs: IDomainConfig[]): void {
|
|
17
|
+
this.settings.domainConfigs = newDomainConfigs;
|
|
18
|
+
|
|
19
|
+
// Reset target indices for removed configs
|
|
20
|
+
const currentConfigSet = new Set(newDomainConfigs);
|
|
21
|
+
for (const [config] of this.domainTargetIndices) {
|
|
22
|
+
if (!currentConfigSet.has(config)) {
|
|
23
|
+
this.domainTargetIndices.delete(config);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get all domain configurations
|
|
30
|
+
*/
|
|
31
|
+
public getDomainConfigs(): IDomainConfig[] {
|
|
32
|
+
return this.settings.domainConfigs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Find domain config matching a server name
|
|
37
|
+
*/
|
|
38
|
+
public findDomainConfig(serverName: string): IDomainConfig | undefined {
|
|
39
|
+
if (!serverName) return undefined;
|
|
40
|
+
|
|
41
|
+
return this.settings.domainConfigs.find((config) =>
|
|
42
|
+
config.domains.some((d) => plugins.minimatch(serverName, d))
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find domain config for a specific port
|
|
48
|
+
*/
|
|
49
|
+
public findDomainConfigForPort(port: number): IDomainConfig | undefined {
|
|
50
|
+
return this.settings.domainConfigs.find(
|
|
51
|
+
(domain) =>
|
|
52
|
+
domain.portRanges &&
|
|
53
|
+
domain.portRanges.length > 0 &&
|
|
54
|
+
this.isPortInRanges(port, domain.portRanges)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if a port is within any of the given ranges
|
|
60
|
+
*/
|
|
61
|
+
public isPortInRanges(port: number, ranges: Array<{ from: number; to: number }>): boolean {
|
|
62
|
+
return ranges.some((range) => port >= range.from && port <= range.to);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get target IP with round-robin support
|
|
67
|
+
*/
|
|
68
|
+
public getTargetIP(domainConfig: IDomainConfig): string {
|
|
69
|
+
if (domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
|
|
70
|
+
const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
|
|
71
|
+
const ip = domainConfig.targetIPs[currentIndex % domainConfig.targetIPs.length];
|
|
72
|
+
this.domainTargetIndices.set(domainConfig, currentIndex + 1);
|
|
73
|
+
return ip;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return this.settings.targetIP || 'localhost';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Checks if a domain should use NetworkProxy
|
|
81
|
+
*/
|
|
82
|
+
public shouldUseNetworkProxy(domainConfig: IDomainConfig): boolean {
|
|
83
|
+
return !!domainConfig.useNetworkProxy;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Gets the NetworkProxy port for a domain
|
|
88
|
+
*/
|
|
89
|
+
public getNetworkProxyPort(domainConfig: IDomainConfig): number | undefined {
|
|
90
|
+
return domainConfig.useNetworkProxy
|
|
91
|
+
? (domainConfig.networkProxyPort || this.settings.networkProxyPort)
|
|
92
|
+
: undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get effective allowed and blocked IPs for a domain
|
|
97
|
+
*/
|
|
98
|
+
public getEffectiveIPRules(domainConfig: IDomainConfig): {
|
|
99
|
+
allowedIPs: string[],
|
|
100
|
+
blockedIPs: string[]
|
|
101
|
+
} {
|
|
102
|
+
return {
|
|
103
|
+
allowedIPs: [
|
|
104
|
+
...domainConfig.allowedIPs,
|
|
105
|
+
...(this.settings.defaultAllowedIPs || [])
|
|
106
|
+
],
|
|
107
|
+
blockedIPs: [
|
|
108
|
+
...(domainConfig.blockedIPs || []),
|
|
109
|
+
...(this.settings.defaultBlockedIPs || [])
|
|
110
|
+
]
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get connection timeout for a domain
|
|
116
|
+
*/
|
|
117
|
+
public getConnectionTimeout(domainConfig?: IDomainConfig): number {
|
|
118
|
+
if (domainConfig?.connectionTimeout) {
|
|
119
|
+
return domainConfig.connectionTimeout;
|
|
120
|
+
}
|
|
121
|
+
return this.settings.maxConnectionLifetime || 86400000; // 24 hours default
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
|
|
3
|
+
/** Domain configuration with per-domain allowed port ranges */
|
|
4
|
+
export interface IDomainConfig {
|
|
5
|
+
domains: string[]; // Glob patterns for domain(s)
|
|
6
|
+
allowedIPs: string[]; // Glob patterns for allowed IPs
|
|
7
|
+
blockedIPs?: string[]; // Glob patterns for blocked IPs
|
|
8
|
+
targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
|
|
9
|
+
portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
|
|
10
|
+
// Allow domain-specific timeout override
|
|
11
|
+
connectionTimeout?: number; // Connection timeout override (ms)
|
|
12
|
+
|
|
13
|
+
// NetworkProxy integration options for this specific domain
|
|
14
|
+
useNetworkProxy?: boolean; // Whether to use NetworkProxy for this domain
|
|
15
|
+
networkProxyPort?: number; // Override default NetworkProxy port for this domain
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Port proxy settings including global allowed port ranges */
|
|
19
|
+
export interface IPortProxySettings {
|
|
20
|
+
fromPort: number;
|
|
21
|
+
toPort: number;
|
|
22
|
+
targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
|
|
23
|
+
domainConfigs: IDomainConfig[];
|
|
24
|
+
sniEnabled?: boolean;
|
|
25
|
+
defaultAllowedIPs?: string[];
|
|
26
|
+
defaultBlockedIPs?: string[];
|
|
27
|
+
preserveSourceIP?: boolean;
|
|
28
|
+
|
|
29
|
+
// TLS options
|
|
30
|
+
pfx?: Buffer;
|
|
31
|
+
key?: string | Buffer | Array<Buffer | string>;
|
|
32
|
+
passphrase?: string;
|
|
33
|
+
cert?: string | Buffer | Array<string | Buffer>;
|
|
34
|
+
ca?: string | Buffer | Array<string | Buffer>;
|
|
35
|
+
ciphers?: string;
|
|
36
|
+
honorCipherOrder?: boolean;
|
|
37
|
+
rejectUnauthorized?: boolean;
|
|
38
|
+
secureProtocol?: string;
|
|
39
|
+
servername?: string;
|
|
40
|
+
minVersion?: string;
|
|
41
|
+
maxVersion?: string;
|
|
42
|
+
|
|
43
|
+
// Timeout settings
|
|
44
|
+
initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
|
|
45
|
+
socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
|
|
46
|
+
inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
|
|
47
|
+
maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h)
|
|
48
|
+
inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
|
|
49
|
+
|
|
50
|
+
gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
|
|
51
|
+
globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
|
|
52
|
+
forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
|
|
53
|
+
|
|
54
|
+
// Socket optimization settings
|
|
55
|
+
noDelay?: boolean; // Disable Nagle's algorithm (default: true)
|
|
56
|
+
keepAlive?: boolean; // Enable TCP keepalive (default: true)
|
|
57
|
+
keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
|
|
58
|
+
maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
|
|
59
|
+
|
|
60
|
+
// Enhanced features
|
|
61
|
+
disableInactivityCheck?: boolean; // Disable inactivity checking entirely
|
|
62
|
+
enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
|
|
63
|
+
enableDetailedLogging?: boolean; // Enable detailed connection logging
|
|
64
|
+
enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
|
|
65
|
+
enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
|
|
66
|
+
allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true)
|
|
67
|
+
|
|
68
|
+
// Rate limiting and security
|
|
69
|
+
maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
|
|
70
|
+
connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
|
|
71
|
+
|
|
72
|
+
// Enhanced keep-alive settings
|
|
73
|
+
keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
|
|
74
|
+
keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
|
|
75
|
+
extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
|
|
76
|
+
|
|
77
|
+
// NetworkProxy integration
|
|
78
|
+
useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
|
|
79
|
+
networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
|
|
80
|
+
|
|
81
|
+
// ACME certificate management options
|
|
82
|
+
acme?: {
|
|
83
|
+
enabled?: boolean; // Whether to enable automatic certificate management
|
|
84
|
+
port?: number; // Port to listen on for ACME challenges (default: 80)
|
|
85
|
+
contactEmail?: string; // Email for Let's Encrypt account
|
|
86
|
+
useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
|
|
87
|
+
renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
|
|
88
|
+
autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
|
|
89
|
+
certificateStore?: string; // Directory to store certificates (default: ./certs)
|
|
90
|
+
skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Enhanced connection record
|
|
96
|
+
*/
|
|
97
|
+
export interface IConnectionRecord {
|
|
98
|
+
id: string; // Unique connection identifier
|
|
99
|
+
incoming: plugins.net.Socket;
|
|
100
|
+
outgoing: plugins.net.Socket | null;
|
|
101
|
+
incomingStartTime: number;
|
|
102
|
+
outgoingStartTime?: number;
|
|
103
|
+
outgoingClosedTime?: number;
|
|
104
|
+
lockedDomain?: string; // Used to lock this connection to the initial SNI
|
|
105
|
+
connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
|
|
106
|
+
cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
|
|
107
|
+
lastActivity: number; // Last activity timestamp for inactivity detection
|
|
108
|
+
pendingData: Buffer[]; // Buffer to hold data during connection setup
|
|
109
|
+
pendingDataSize: number; // Track total size of pending data
|
|
110
|
+
|
|
111
|
+
// Enhanced tracking fields
|
|
112
|
+
bytesReceived: number; // Total bytes received
|
|
113
|
+
bytesSent: number; // Total bytes sent
|
|
114
|
+
remoteIP: string; // Remote IP (cached for logging after socket close)
|
|
115
|
+
localPort: number; // Local port (cached for logging)
|
|
116
|
+
isTLS: boolean; // Whether this connection is a TLS connection
|
|
117
|
+
tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
|
|
118
|
+
hasReceivedInitialData: boolean; // Whether initial data has been received
|
|
119
|
+
domainConfig?: IDomainConfig; // Associated domain config for this connection
|
|
120
|
+
|
|
121
|
+
// Keep-alive tracking
|
|
122
|
+
hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
|
|
123
|
+
inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
|
|
124
|
+
incomingTerminationReason?: string | null; // Reason for incoming termination
|
|
125
|
+
outgoingTerminationReason?: string | null; // Reason for outgoing termination
|
|
126
|
+
|
|
127
|
+
// NetworkProxy tracking
|
|
128
|
+
usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
|
|
129
|
+
|
|
130
|
+
// Renegotiation handler
|
|
131
|
+
renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
|
|
132
|
+
|
|
133
|
+
// Browser connection tracking
|
|
134
|
+
isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
|
|
135
|
+
domainSwitches?: number; // Number of times the domain has been switched on this connection
|
|
136
|
+
}
|