@push.rocks/smartproxy 3.41.7 → 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.
Files changed (44) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/classes.portproxy.js +83 -69
  3. package/dist_ts/classes.pp.acmemanager.d.ts +34 -0
  4. package/dist_ts/classes.pp.acmemanager.js +123 -0
  5. package/dist_ts/classes.pp.connectionhandler.d.ts +39 -0
  6. package/dist_ts/classes.pp.connectionhandler.js +693 -0
  7. package/dist_ts/classes.pp.connectionmanager.d.ts +78 -0
  8. package/dist_ts/classes.pp.connectionmanager.js +378 -0
  9. package/dist_ts/classes.pp.domainconfigmanager.d.ts +55 -0
  10. package/dist_ts/classes.pp.domainconfigmanager.js +103 -0
  11. package/dist_ts/classes.pp.interfaces.d.ts +109 -0
  12. package/dist_ts/classes.pp.interfaces.js +2 -0
  13. package/dist_ts/classes.pp.networkproxybridge.d.ts +43 -0
  14. package/dist_ts/classes.pp.networkproxybridge.js +211 -0
  15. package/dist_ts/classes.pp.portproxy.d.ts +48 -0
  16. package/dist_ts/classes.pp.portproxy.js +268 -0
  17. package/dist_ts/classes.pp.portrangemanager.d.ts +56 -0
  18. package/dist_ts/classes.pp.portrangemanager.js +179 -0
  19. package/dist_ts/classes.pp.securitymanager.d.ts +47 -0
  20. package/dist_ts/classes.pp.securitymanager.js +126 -0
  21. package/dist_ts/classes.pp.snihandler.d.ts +160 -0
  22. package/dist_ts/classes.pp.snihandler.js +1073 -0
  23. package/dist_ts/classes.pp.timeoutmanager.d.ts +47 -0
  24. package/dist_ts/classes.pp.timeoutmanager.js +154 -0
  25. package/dist_ts/classes.pp.tlsmanager.d.ts +57 -0
  26. package/dist_ts/classes.pp.tlsmanager.js +132 -0
  27. package/dist_ts/index.d.ts +2 -2
  28. package/dist_ts/index.js +3 -3
  29. package/package.json +1 -1
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/classes.pp.acmemanager.ts +149 -0
  32. package/ts/classes.pp.connectionhandler.ts +982 -0
  33. package/ts/classes.pp.connectionmanager.ts +446 -0
  34. package/ts/classes.pp.domainconfigmanager.ts +123 -0
  35. package/ts/classes.pp.interfaces.ts +136 -0
  36. package/ts/classes.pp.networkproxybridge.ts +258 -0
  37. package/ts/classes.pp.portproxy.ts +344 -0
  38. package/ts/classes.pp.portrangemanager.ts +214 -0
  39. package/ts/classes.pp.securitymanager.ts +147 -0
  40. package/ts/{classes.snihandler.ts → classes.pp.snihandler.ts} +2 -169
  41. package/ts/classes.pp.timeoutmanager.ts +190 -0
  42. package/ts/classes.pp.tlsmanager.ts +206 -0
  43. package/ts/index.ts +2 -2
  44. package/ts/classes.portproxy.ts +0 -2496
@@ -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
+ }