@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
@@ -1,2496 +0,0 @@
1
- import * as plugins from './plugins.js';
2
- import { NetworkProxy } from './classes.networkproxy.js';
3
- import { SniHandler } from './classes.snihandler.js';
4
-
5
- /** Domain configuration with per-domain allowed port ranges */
6
- export interface IDomainConfig {
7
- domains: string[]; // Glob patterns for domain(s)
8
- allowedIPs: string[]; // Glob patterns for allowed IPs
9
- blockedIPs?: string[]; // Glob patterns for blocked IPs
10
- targetIPs?: string[]; // If multiple targetIPs are given, use round robin.
11
- portRanges?: Array<{ from: number; to: number }>; // Optional port ranges
12
- // Allow domain-specific timeout override
13
- connectionTimeout?: number; // Connection timeout override (ms)
14
-
15
- // NetworkProxy integration options for this specific domain
16
- useNetworkProxy?: boolean; // Whether to use NetworkProxy for this domain
17
- networkProxyPort?: number; // Override default NetworkProxy port for this domain
18
- }
19
-
20
- /** Port proxy settings including global allowed port ranges */
21
- export interface IPortProxySettings extends plugins.tls.TlsOptions {
22
- fromPort: number;
23
- toPort: number;
24
- targetIP?: string; // Global target host to proxy to, defaults to 'localhost'
25
- domainConfigs: IDomainConfig[];
26
- sniEnabled?: boolean;
27
- defaultAllowedIPs?: string[];
28
- defaultBlockedIPs?: string[];
29
- preserveSourceIP?: boolean;
30
-
31
- // Timeout settings
32
- initialDataTimeout?: number; // Timeout for initial data/SNI (ms), default: 60000 (60s)
33
- socketTimeout?: number; // Socket inactivity timeout (ms), default: 3600000 (1h)
34
- inactivityCheckInterval?: number; // How often to check for inactive connections (ms), default: 60000 (60s)
35
- maxConnectionLifetime?: number; // Default max connection lifetime (ms), default: 86400000 (24h)
36
- inactivityTimeout?: number; // Inactivity timeout (ms), default: 14400000 (4h)
37
-
38
- gracefulShutdownTimeout?: number; // (ms) maximum time to wait for connections to close during shutdown
39
- globalPortRanges: Array<{ from: number; to: number }>; // Global allowed port ranges
40
- forwardAllGlobalRanges?: boolean; // When true, forwards all connections on global port ranges to the global targetIP
41
-
42
- // Socket optimization settings
43
- noDelay?: boolean; // Disable Nagle's algorithm (default: true)
44
- keepAlive?: boolean; // Enable TCP keepalive (default: true)
45
- keepAliveInitialDelay?: number; // Initial delay before sending keepalive probes (ms)
46
- maxPendingDataSize?: number; // Maximum bytes to buffer during connection setup
47
-
48
- // Enhanced features
49
- disableInactivityCheck?: boolean; // Disable inactivity checking entirely
50
- enableKeepAliveProbes?: boolean; // Enable TCP keep-alive probes
51
- enableDetailedLogging?: boolean; // Enable detailed connection logging
52
- enableTlsDebugLogging?: boolean; // Enable TLS handshake debug logging
53
- enableRandomizedTimeouts?: boolean; // Randomize timeouts slightly to prevent thundering herd
54
- allowSessionTicket?: boolean; // Allow TLS session ticket for reconnection (default: true)
55
-
56
- // Rate limiting and security
57
- maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
58
- connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
59
-
60
- // Enhanced keep-alive settings
61
- keepAliveTreatment?: 'standard' | 'extended' | 'immortal'; // How to treat keep-alive connections
62
- keepAliveInactivityMultiplier?: number; // Multiplier for inactivity timeout for keep-alive connections
63
- extendedKeepAliveLifetime?: number; // Extended lifetime for keep-alive connections (ms)
64
-
65
- // NetworkProxy integration
66
- useNetworkProxy?: number[]; // Array of ports to forward to NetworkProxy
67
- networkProxyPort?: number; // Port where NetworkProxy is listening (default: 8443)
68
-
69
- // ACME certificate management options
70
- acme?: {
71
- enabled?: boolean; // Whether to enable automatic certificate management
72
- port?: number; // Port to listen on for ACME challenges (default: 80)
73
- contactEmail?: string; // Email for Let's Encrypt account
74
- useProduction?: boolean; // Whether to use Let's Encrypt production (default: false for staging)
75
- renewThresholdDays?: number; // Days before expiry to renew certificates (default: 30)
76
- autoRenew?: boolean; // Whether to automatically renew certificates (default: true)
77
- certificateStore?: string; // Directory to store certificates (default: ./certs)
78
- skipConfiguredCerts?: boolean; // Skip domains that already have certificates configured
79
- };
80
- }
81
-
82
- /**
83
- * Enhanced connection record
84
- */
85
- interface IConnectionRecord {
86
- id: string; // Unique connection identifier
87
- incoming: plugins.net.Socket;
88
- outgoing: plugins.net.Socket | null;
89
- incomingStartTime: number;
90
- outgoingStartTime?: number;
91
- outgoingClosedTime?: number;
92
- lockedDomain?: string; // Used to lock this connection to the initial SNI
93
- connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
94
- cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
95
- lastActivity: number; // Last activity timestamp for inactivity detection
96
- pendingData: Buffer[]; // Buffer to hold data during connection setup
97
- pendingDataSize: number; // Track total size of pending data
98
-
99
- // Enhanced tracking fields
100
- bytesReceived: number; // Total bytes received
101
- bytesSent: number; // Total bytes sent
102
- remoteIP: string; // Remote IP (cached for logging after socket close)
103
- localPort: number; // Local port (cached for logging)
104
- isTLS: boolean; // Whether this connection is a TLS connection
105
- tlsHandshakeComplete: boolean; // Whether the TLS handshake is complete
106
- hasReceivedInitialData: boolean; // Whether initial data has been received
107
- domainConfig?: IDomainConfig; // Associated domain config for this connection
108
-
109
- // Keep-alive tracking
110
- hasKeepAlive: boolean; // Whether keep-alive is enabled for this connection
111
- inactivityWarningIssued?: boolean; // Whether an inactivity warning has been issued
112
- incomingTerminationReason?: string | null; // Reason for incoming termination
113
- outgoingTerminationReason?: string | null; // Reason for outgoing termination
114
-
115
- // NetworkProxy tracking
116
- usingNetworkProxy?: boolean; // Whether this connection is using a NetworkProxy
117
-
118
- // Renegotiation handler
119
- renegotiationHandler?: (chunk: Buffer) => void; // Handler for renegotiation detection
120
-
121
- // Browser connection tracking
122
- isBrowserConnection?: boolean; // Whether this connection appears to be from a browser
123
- domainSwitches?: number; // Number of times the domain has been switched on this connection
124
- }
125
-
126
- // SNI functions are now imported from SniHandler class
127
- // No need for wrapper functions
128
-
129
- // Helper: Check if a port falls within any of the given port ranges
130
- const isPortInRanges = (port: number, ranges: Array<{ from: number; to: number }>): boolean => {
131
- return ranges.some((range) => port >= range.from && port <= range.to);
132
- };
133
-
134
- // Helper: Check if a given IP matches any of the glob patterns
135
- const isAllowed = (ip: string, patterns: string[]): boolean => {
136
- if (!ip || !patterns || patterns.length === 0) return false;
137
-
138
- const normalizeIP = (ip: string): string[] => {
139
- if (!ip) return [];
140
- if (ip.startsWith('::ffff:')) {
141
- const ipv4 = ip.slice(7);
142
- return [ip, ipv4];
143
- }
144
- if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
145
- return [ip, `::ffff:${ip}`];
146
- }
147
- return [ip];
148
- };
149
-
150
- const normalizedIPVariants = normalizeIP(ip);
151
- if (normalizedIPVariants.length === 0) return false;
152
-
153
- const expandedPatterns = patterns.flatMap(normalizeIP);
154
- return normalizedIPVariants.some((ipVariant) =>
155
- expandedPatterns.some((pattern) => plugins.minimatch(ipVariant, pattern))
156
- );
157
- };
158
-
159
- // Helper: Check if an IP is allowed considering allowed and blocked glob patterns
160
- const isGlobIPAllowed = (ip: string, allowed: string[], blocked: string[] = []): boolean => {
161
- if (!ip) return false;
162
- if (blocked.length > 0 && isAllowed(ip, blocked)) return false;
163
- return isAllowed(ip, allowed);
164
- };
165
-
166
- // Helper: Generate a unique connection ID
167
- const generateConnectionId = (): string => {
168
- return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
169
- };
170
-
171
- // SNI functions are now imported from SniHandler class
172
-
173
- // Helper: Ensure timeout values don't exceed Node.js max safe integer
174
- const ensureSafeTimeout = (timeout: number): number => {
175
- const MAX_SAFE_TIMEOUT = 2147483647; // Maximum safe value (2^31 - 1)
176
- return Math.min(Math.floor(timeout), MAX_SAFE_TIMEOUT);
177
- };
178
-
179
- // Helper: Generate a slightly randomized timeout to prevent thundering herd
180
- const randomizeTimeout = (baseTimeout: number, variationPercent: number = 5): number => {
181
- const safeBaseTimeout = ensureSafeTimeout(baseTimeout);
182
- const variation = safeBaseTimeout * (variationPercent / 100);
183
- return ensureSafeTimeout(safeBaseTimeout + Math.floor(Math.random() * variation * 2) - variation);
184
- };
185
-
186
- export class PortProxy {
187
- private netServers: plugins.net.Server[] = [];
188
- settings: IPortProxySettings;
189
- private connectionRecords: Map<string, IConnectionRecord> = new Map();
190
- private connectionLogger: NodeJS.Timeout | null = null;
191
- private isShuttingDown: boolean = false;
192
-
193
- // Map to track round robin indices for each domain config
194
- private domainTargetIndices: Map<IDomainConfig, number> = new Map();
195
-
196
- // Enhanced stats tracking
197
- private terminationStats: {
198
- incoming: Record<string, number>;
199
- outgoing: Record<string, number>;
200
- } = {
201
- incoming: {},
202
- outgoing: {},
203
- };
204
-
205
- // Connection tracking by IP for rate limiting
206
- private connectionsByIP: Map<string, Set<string>> = new Map();
207
- private connectionRateByIP: Map<string, number[]> = new Map();
208
-
209
- // NetworkProxy instance for TLS termination
210
- private networkProxy: NetworkProxy | null = null;
211
-
212
- constructor(settingsArg: IPortProxySettings) {
213
- // Set reasonable defaults for all settings
214
- this.settings = {
215
- ...settingsArg,
216
- targetIP: settingsArg.targetIP || 'localhost',
217
-
218
- // Timeout settings with reasonable defaults
219
- initialDataTimeout: settingsArg.initialDataTimeout || 60000, // 60 seconds for initial handshake
220
- socketTimeout: ensureSafeTimeout(settingsArg.socketTimeout || 3600000), // 1 hour socket timeout
221
- inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, // 60 seconds interval
222
- maxConnectionLifetime: ensureSafeTimeout(settingsArg.maxConnectionLifetime || 86400000), // 24 hours default
223
- inactivityTimeout: ensureSafeTimeout(settingsArg.inactivityTimeout || 14400000), // 4 hours inactivity timeout
224
-
225
- gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, // 30 seconds
226
-
227
- // Socket optimization settings
228
- noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true,
229
- keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true,
230
- keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, // 10 seconds
231
- maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, // 10MB
232
-
233
- // Feature flags
234
- disableInactivityCheck: settingsArg.disableInactivityCheck || false,
235
- enableKeepAliveProbes:
236
- settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true,
237
- enableDetailedLogging: settingsArg.enableDetailedLogging || false,
238
- enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false,
239
- enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false,
240
- allowSessionTicket:
241
- settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true,
242
-
243
- // Rate limiting defaults
244
- maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100,
245
- connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300,
246
-
247
- // Enhanced keep-alive settings
248
- keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended',
249
- keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6,
250
- extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, // 7 days
251
-
252
- // NetworkProxy settings
253
- networkProxyPort: settingsArg.networkProxyPort || 8443, // Default NetworkProxy port
254
-
255
- // ACME certificate settings with reasonable defaults
256
- acme: settingsArg.acme || {
257
- enabled: false,
258
- port: 80,
259
- contactEmail: 'admin@example.com',
260
- useProduction: false,
261
- renewThresholdDays: 30,
262
- autoRenew: true,
263
- certificateStore: './certs',
264
- skipConfiguredCerts: false,
265
- },
266
- };
267
-
268
- // Initialize NetworkProxy if enabled
269
- if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
270
- this.initializeNetworkProxy();
271
- }
272
- }
273
-
274
- /**
275
- * Initialize NetworkProxy instance
276
- */
277
- private async initializeNetworkProxy(): Promise<void> {
278
- if (!this.networkProxy) {
279
- // Configure NetworkProxy options based on PortProxy settings
280
- const networkProxyOptions: any = {
281
- port: this.settings.networkProxyPort!,
282
- portProxyIntegration: true,
283
- logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info',
284
- };
285
-
286
- // Add ACME settings if configured
287
- if (this.settings.acme) {
288
- networkProxyOptions.acme = { ...this.settings.acme };
289
- }
290
-
291
- this.networkProxy = new NetworkProxy(networkProxyOptions);
292
-
293
- console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
294
-
295
- // Convert and apply domain configurations to NetworkProxy
296
- await this.syncDomainConfigsToNetworkProxy();
297
- }
298
- }
299
-
300
- /**
301
- * Updates the domain configurations for the proxy
302
- * @param newDomainConfigs The new domain configurations
303
- */
304
- public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise<void> {
305
- console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`);
306
- this.settings.domainConfigs = newDomainConfigs;
307
-
308
- // If NetworkProxy is initialized, resync the configurations
309
- if (this.networkProxy) {
310
- await this.syncDomainConfigsToNetworkProxy();
311
- }
312
- }
313
-
314
- /**
315
- * Updates the ACME certificate settings
316
- * @param acmeSettings New ACME settings
317
- */
318
- public async updateAcmeSettings(acmeSettings: IPortProxySettings['acme']): Promise<void> {
319
- console.log('Updating ACME certificate settings');
320
-
321
- // Update settings
322
- this.settings.acme = {
323
- ...this.settings.acme,
324
- ...acmeSettings,
325
- };
326
-
327
- // If NetworkProxy is initialized, update its ACME settings
328
- if (this.networkProxy) {
329
- try {
330
- // Recreate NetworkProxy with new settings if ACME enabled state has changed
331
- if (this.settings.acme.enabled !== acmeSettings.enabled) {
332
- console.log(`ACME enabled state changed to: ${acmeSettings.enabled}`);
333
-
334
- // Stop the current NetworkProxy
335
- await this.networkProxy.stop();
336
- this.networkProxy = null;
337
-
338
- // Reinitialize with new settings
339
- await this.initializeNetworkProxy();
340
-
341
- // Use start() to make sure ACME gets initialized if newly enabled
342
- await this.networkProxy.start();
343
- } else {
344
- // Update existing NetworkProxy with new settings
345
- // Note: Some settings may require a restart to take effect
346
- console.log('Updating ACME settings in NetworkProxy');
347
-
348
- // For certificate renewals, we might want to trigger checks with the new settings
349
- if (acmeSettings.renewThresholdDays) {
350
- console.log(`Setting new renewal threshold to ${acmeSettings.renewThresholdDays} days`);
351
- // This is implementation-dependent but gives an example
352
- if (this.networkProxy.options.acme) {
353
- this.networkProxy.options.acme.renewThresholdDays = acmeSettings.renewThresholdDays;
354
- }
355
- }
356
- }
357
- } catch (err) {
358
- console.log(`Error updating ACME settings: ${err}`);
359
- }
360
- }
361
- }
362
-
363
- /**
364
- * Synchronizes PortProxy domain configurations to NetworkProxy
365
- * This allows domains configured in PortProxy to be used by NetworkProxy
366
- */
367
- private async syncDomainConfigsToNetworkProxy(): Promise<void> {
368
- if (!this.networkProxy) {
369
- console.log('Cannot sync configurations - NetworkProxy not initialized');
370
- return;
371
- }
372
-
373
- try {
374
- // Get SSL certificates from assets
375
- // Import fs directly since it's not in plugins
376
- const fs = await import('fs');
377
-
378
- let certPair;
379
- try {
380
- certPair = {
381
- key: fs.readFileSync('assets/certs/key.pem', 'utf8'),
382
- cert: fs.readFileSync('assets/certs/cert.pem', 'utf8'),
383
- };
384
- } catch (certError) {
385
- console.log(`Warning: Could not read default certificates: ${certError}`);
386
- console.log(
387
- 'Using empty certificate placeholders - ACME will generate proper certificates if enabled'
388
- );
389
-
390
- // Use empty placeholders - NetworkProxy will use its internal defaults
391
- // or ACME will generate proper ones if enabled
392
- certPair = {
393
- key: '',
394
- cert: '',
395
- };
396
- }
397
-
398
- // Convert domain configs to NetworkProxy configs
399
- const proxyConfigs = this.networkProxy.convertPortProxyConfigs(
400
- this.settings.domainConfigs,
401
- certPair
402
- );
403
-
404
- // Log ACME-eligible domains if ACME is enabled
405
- if (this.settings.acme?.enabled) {
406
- const acmeEligibleDomains = proxyConfigs
407
- .filter((config) => !config.hostName.includes('*')) // Exclude wildcards
408
- .map((config) => config.hostName);
409
-
410
- if (acmeEligibleDomains.length > 0) {
411
- console.log(`Domains eligible for ACME certificates: ${acmeEligibleDomains.join(', ')}`);
412
- } else {
413
- console.log('No domains eligible for ACME certificates found in configuration');
414
- }
415
- }
416
-
417
- // Update NetworkProxy with the converted configs
418
- this.networkProxy
419
- .updateProxyConfigs(proxyConfigs)
420
- .then(() => {
421
- console.log(
422
- `Successfully synchronized ${proxyConfigs.length} domain configurations to NetworkProxy`
423
- );
424
- })
425
- .catch((err) => {
426
- console.log(`Error synchronizing configurations: ${err.message}`);
427
- });
428
- } catch (err) {
429
- console.log(`Failed to sync configurations: ${err}`);
430
- }
431
- }
432
-
433
- /**
434
- * Requests a certificate for a specific domain
435
- * @param domain The domain to request a certificate for
436
- * @returns Promise that resolves to true if the request was successful, false otherwise
437
- */
438
- public async requestCertificate(domain: string): Promise<boolean> {
439
- if (!this.networkProxy) {
440
- console.log('Cannot request certificate - NetworkProxy not initialized');
441
- return false;
442
- }
443
-
444
- if (!this.settings.acme?.enabled) {
445
- console.log('Cannot request certificate - ACME is not enabled');
446
- return false;
447
- }
448
-
449
- try {
450
- const result = await this.networkProxy.requestCertificate(domain);
451
- if (result) {
452
- console.log(`Certificate request for ${domain} submitted successfully`);
453
- } else {
454
- console.log(`Certificate request for ${domain} failed`);
455
- }
456
- return result;
457
- } catch (err) {
458
- console.log(`Error requesting certificate: ${err}`);
459
- return false;
460
- }
461
- }
462
-
463
- /**
464
- * Forwards a TLS connection to a NetworkProxy for handling
465
- * @param connectionId - Unique connection identifier
466
- * @param socket - The incoming client socket
467
- * @param record - The connection record
468
- * @param initialData - Initial data chunk (TLS ClientHello)
469
- * @param customProxyPort - Optional custom port for NetworkProxy (for domain-specific settings)
470
- */
471
- private forwardToNetworkProxy(
472
- connectionId: string,
473
- socket: plugins.net.Socket,
474
- record: IConnectionRecord,
475
- initialData: Buffer,
476
- customProxyPort?: number
477
- ): void {
478
- // Ensure NetworkProxy is initialized
479
- if (!this.networkProxy) {
480
- console.log(
481
- `[${connectionId}] NetworkProxy not initialized. Using fallback direct connection.`
482
- );
483
- // Fall back to direct connection
484
- return this.setupDirectConnection(
485
- connectionId,
486
- socket,
487
- record,
488
- undefined,
489
- undefined,
490
- initialData
491
- );
492
- }
493
-
494
- // Use the custom port if provided, otherwise use the default NetworkProxy port
495
- const proxyPort = customProxyPort || this.networkProxy.getListeningPort();
496
- const proxyHost = 'localhost'; // Assuming NetworkProxy runs locally
497
-
498
- if (this.settings.enableDetailedLogging) {
499
- console.log(
500
- `[${connectionId}] Forwarding TLS connection to NetworkProxy at ${proxyHost}:${proxyPort}`
501
- );
502
- }
503
-
504
- // Create a connection to the NetworkProxy
505
- const proxySocket = plugins.net.connect({
506
- host: proxyHost,
507
- port: proxyPort,
508
- });
509
-
510
- // Store the outgoing socket in the record
511
- record.outgoing = proxySocket;
512
- record.outgoingStartTime = Date.now();
513
- record.usingNetworkProxy = true;
514
-
515
- // Set up error handlers
516
- proxySocket.on('error', (err) => {
517
- console.log(`[${connectionId}] Error connecting to NetworkProxy: ${err.message}`);
518
- this.cleanupConnection(record, 'network_proxy_connect_error');
519
- });
520
-
521
- // Handle connection to NetworkProxy
522
- proxySocket.on('connect', () => {
523
- if (this.settings.enableDetailedLogging) {
524
- console.log(`[${connectionId}] Connected to NetworkProxy at ${proxyHost}:${proxyPort}`);
525
- }
526
-
527
- // First send the initial data that contains the TLS ClientHello
528
- proxySocket.write(initialData);
529
-
530
- // Now set up bidirectional piping between client and NetworkProxy
531
- socket.pipe(proxySocket);
532
- proxySocket.pipe(socket);
533
-
534
- // Setup cleanup handlers
535
- proxySocket.on('close', () => {
536
- if (this.settings.enableDetailedLogging) {
537
- console.log(`[${connectionId}] NetworkProxy connection closed`);
538
- }
539
- this.cleanupConnection(record, 'network_proxy_closed');
540
- });
541
-
542
- socket.on('close', () => {
543
- if (this.settings.enableDetailedLogging) {
544
- console.log(
545
- `[${connectionId}] Client connection closed after forwarding to NetworkProxy`
546
- );
547
- }
548
- this.cleanupConnection(record, 'client_closed');
549
- });
550
-
551
- // Update activity on data transfer
552
- socket.on('data', () => this.updateActivity(record));
553
- proxySocket.on('data', () => this.updateActivity(record));
554
-
555
- if (this.settings.enableDetailedLogging) {
556
- console.log(`[${connectionId}] TLS connection successfully forwarded to NetworkProxy`);
557
- }
558
- });
559
- }
560
-
561
- /**
562
- * Sets up a direct connection to the target (original behavior)
563
- * This is used when NetworkProxy isn't configured or as a fallback
564
- */
565
- private setupDirectConnection(
566
- connectionId: string,
567
- socket: plugins.net.Socket,
568
- record: IConnectionRecord,
569
- domainConfig: IDomainConfig | undefined,
570
- serverName?: string,
571
- initialChunk?: Buffer,
572
- overridePort?: number
573
- ): void {
574
- // Existing connection setup logic
575
- const targetHost = domainConfig ? this.getTargetIP(domainConfig) : this.settings.targetIP!;
576
- const connectionOptions: plugins.net.NetConnectOpts = {
577
- host: targetHost,
578
- port: overridePort !== undefined ? overridePort : this.settings.toPort,
579
- };
580
- if (this.settings.preserveSourceIP) {
581
- connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
582
- }
583
-
584
- // Create a safe queue for incoming data using a Buffer array
585
- // We'll use this to ensure we don't lose data during handler transitions
586
- const dataQueue: Buffer[] = [];
587
- let queueSize = 0;
588
- let processingQueue = false;
589
- let drainPending = false;
590
-
591
- // Flag to track if we've switched to the final piping mechanism
592
- // Once this is true, we no longer buffer data in dataQueue
593
- let pipingEstablished = false;
594
-
595
- // Pause the incoming socket to prevent buffer overflows
596
- // This ensures we control the flow of data until piping is set up
597
- socket.pause();
598
-
599
- // Function to safely process the data queue without losing events
600
- const processDataQueue = () => {
601
- if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
602
-
603
- processingQueue = true;
604
-
605
- try {
606
- // Process all queued chunks with the current active handler
607
- while (dataQueue.length > 0) {
608
- const chunk = dataQueue.shift()!;
609
- queueSize -= chunk.length;
610
-
611
- // Once piping is established, we shouldn't get here,
612
- // but just in case, pass to the outgoing socket directly
613
- if (pipingEstablished && record.outgoing) {
614
- record.outgoing.write(chunk);
615
- continue;
616
- }
617
-
618
- // Track bytes received
619
- record.bytesReceived += chunk.length;
620
-
621
- // Check for TLS handshake
622
- if (!record.isTLS && SniHandler.isTlsHandshake(chunk)) {
623
- record.isTLS = true;
624
-
625
- if (this.settings.enableTlsDebugLogging) {
626
- console.log(
627
- `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
628
- );
629
- }
630
- }
631
-
632
- // Check if adding this chunk would exceed the buffer limit
633
- const newSize = record.pendingDataSize + chunk.length;
634
-
635
- if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
636
- console.log(
637
- `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
638
- );
639
- socket.end(); // Gracefully close the socket
640
- this.initiateCleanupOnce(record, 'buffer_limit_exceeded');
641
- return;
642
- }
643
-
644
- // Buffer the chunk and update the size counter
645
- record.pendingData.push(Buffer.from(chunk));
646
- record.pendingDataSize = newSize;
647
- this.updateActivity(record);
648
- }
649
- } finally {
650
- processingQueue = false;
651
-
652
- // If there's a pending drain and we've processed everything,
653
- // signal we're ready for more data if we haven't established piping yet
654
- if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
655
- drainPending = false;
656
- socket.resume();
657
- }
658
- }
659
- };
660
-
661
- // Unified data handler that safely queues incoming data
662
- const safeDataHandler = (chunk: Buffer) => {
663
- // If piping is already established, just let the pipe handle it
664
- if (pipingEstablished) return;
665
-
666
- // Add to our queue for orderly processing
667
- dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
668
- queueSize += chunk.length;
669
-
670
- // If queue is getting large, pause socket until we catch up
671
- if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
672
- socket.pause();
673
- drainPending = true;
674
- }
675
-
676
- // Process the queue
677
- processDataQueue();
678
- };
679
-
680
- // Add our safe data handler
681
- socket.on('data', safeDataHandler);
682
-
683
- // Add initial chunk to pending data if present
684
- if (initialChunk) {
685
- record.bytesReceived += initialChunk.length;
686
- record.pendingData.push(Buffer.from(initialChunk));
687
- record.pendingDataSize = initialChunk.length;
688
- }
689
-
690
- // Create the target socket but don't set up piping immediately
691
- const targetSocket = plugins.net.connect(connectionOptions);
692
- record.outgoing = targetSocket;
693
- record.outgoingStartTime = Date.now();
694
-
695
- // Apply socket optimizations
696
- targetSocket.setNoDelay(this.settings.noDelay);
697
-
698
- // Apply keep-alive settings to the outgoing connection as well
699
- if (this.settings.keepAlive) {
700
- targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
701
-
702
- // Apply enhanced TCP keep-alive options if enabled
703
- if (this.settings.enableKeepAliveProbes) {
704
- try {
705
- if ('setKeepAliveProbes' in targetSocket) {
706
- (targetSocket as any).setKeepAliveProbes(10);
707
- }
708
- if ('setKeepAliveInterval' in targetSocket) {
709
- (targetSocket as any).setKeepAliveInterval(1000);
710
- }
711
- } catch (err) {
712
- // Ignore errors - these are optional enhancements
713
- if (this.settings.enableDetailedLogging) {
714
- console.log(
715
- `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
716
- );
717
- }
718
- }
719
- }
720
- }
721
-
722
- // Setup specific error handler for connection phase
723
- targetSocket.once('error', (err) => {
724
- // This handler runs only once during the initial connection phase
725
- const code = (err as any).code;
726
- console.log(
727
- `[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`
728
- );
729
-
730
- // Resume the incoming socket to prevent it from hanging
731
- socket.resume();
732
-
733
- if (code === 'ECONNREFUSED') {
734
- console.log(
735
- `[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
736
- );
737
- } else if (code === 'ETIMEDOUT') {
738
- console.log(
739
- `[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`
740
- );
741
- } else if (code === 'ECONNRESET') {
742
- console.log(
743
- `[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`
744
- );
745
- } else if (code === 'EHOSTUNREACH') {
746
- console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
747
- }
748
-
749
- // Clear any existing error handler after connection phase
750
- targetSocket.removeAllListeners('error');
751
-
752
- // Re-add the normal error handler for established connections
753
- targetSocket.on('error', this.handleError('outgoing', record));
754
-
755
- if (record.outgoingTerminationReason === null) {
756
- record.outgoingTerminationReason = 'connection_failed';
757
- this.incrementTerminationStat('outgoing', 'connection_failed');
758
- }
759
-
760
- // Clean up the connection
761
- this.initiateCleanupOnce(record, `connection_failed_${code}`);
762
- });
763
-
764
- // Setup close handler
765
- targetSocket.on('close', this.handleClose('outgoing', record));
766
- socket.on('close', this.handleClose('incoming', record));
767
-
768
- // Handle timeouts with keep-alive awareness
769
- socket.on('timeout', () => {
770
- // For keep-alive connections, just log a warning instead of closing
771
- if (record.hasKeepAlive) {
772
- console.log(
773
- `[${connectionId}] Timeout event on incoming keep-alive connection from ${
774
- record.remoteIP
775
- } after ${plugins.prettyMs(
776
- this.settings.socketTimeout || 3600000
777
- )}. Connection preserved.`
778
- );
779
- // Don't close the connection - just log
780
- return;
781
- }
782
-
783
- // For non-keep-alive connections, proceed with normal cleanup
784
- console.log(
785
- `[${connectionId}] Timeout on incoming side from ${
786
- record.remoteIP
787
- } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
788
- );
789
- if (record.incomingTerminationReason === null) {
790
- record.incomingTerminationReason = 'timeout';
791
- this.incrementTerminationStat('incoming', 'timeout');
792
- }
793
- this.initiateCleanupOnce(record, 'timeout_incoming');
794
- });
795
-
796
- targetSocket.on('timeout', () => {
797
- // For keep-alive connections, just log a warning instead of closing
798
- if (record.hasKeepAlive) {
799
- console.log(
800
- `[${connectionId}] Timeout event on outgoing keep-alive connection from ${
801
- record.remoteIP
802
- } after ${plugins.prettyMs(
803
- this.settings.socketTimeout || 3600000
804
- )}. Connection preserved.`
805
- );
806
- // Don't close the connection - just log
807
- return;
808
- }
809
-
810
- // For non-keep-alive connections, proceed with normal cleanup
811
- console.log(
812
- `[${connectionId}] Timeout on outgoing side from ${
813
- record.remoteIP
814
- } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
815
- );
816
- if (record.outgoingTerminationReason === null) {
817
- record.outgoingTerminationReason = 'timeout';
818
- this.incrementTerminationStat('outgoing', 'timeout');
819
- }
820
- this.initiateCleanupOnce(record, 'timeout_outgoing');
821
- });
822
-
823
- // Set appropriate timeouts, or disable for immortal keep-alive connections
824
- if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
825
- // Disable timeouts completely for immortal connections
826
- socket.setTimeout(0);
827
- targetSocket.setTimeout(0);
828
-
829
- if (this.settings.enableDetailedLogging) {
830
- console.log(
831
- `[${connectionId}] Disabled socket timeouts for immortal keep-alive connection`
832
- );
833
- }
834
- } else {
835
- // Set normal timeouts for other connections
836
- socket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
837
- targetSocket.setTimeout(ensureSafeTimeout(this.settings.socketTimeout || 3600000));
838
- }
839
-
840
- // Track outgoing data for bytes counting
841
- targetSocket.on('data', (chunk: Buffer) => {
842
- record.bytesSent += chunk.length;
843
- this.updateActivity(record);
844
- });
845
-
846
- // Wait for the outgoing connection to be ready before setting up piping
847
- targetSocket.once('connect', () => {
848
- // Clear the initial connection error handler
849
- targetSocket.removeAllListeners('error');
850
-
851
- // Add the normal error handler for established connections
852
- targetSocket.on('error', this.handleError('outgoing', record));
853
-
854
- // Process any remaining data in the queue before switching to piping
855
- processDataQueue();
856
-
857
- // Setup function to establish piping - we'll use this after flushing data
858
- const setupPiping = () => {
859
- // Mark that we're switching to piping mode
860
- pipingEstablished = true;
861
-
862
- // Setup piping in both directions
863
- socket.pipe(targetSocket);
864
- targetSocket.pipe(socket);
865
-
866
- // Resume the socket to ensure data flows
867
- socket.resume();
868
-
869
- // Process any data that might be queued in the interim
870
- if (dataQueue.length > 0) {
871
- // Write any remaining queued data directly to the target socket
872
- for (const chunk of dataQueue) {
873
- targetSocket.write(chunk);
874
- }
875
- // Clear the queue
876
- dataQueue.length = 0;
877
- queueSize = 0;
878
- }
879
-
880
- if (this.settings.enableDetailedLogging) {
881
- console.log(
882
- `[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
883
- `${
884
- serverName
885
- ? ` (SNI: ${serverName})`
886
- : domainConfig
887
- ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
888
- : ''
889
- }` +
890
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
891
- record.hasKeepAlive ? 'Yes' : 'No'
892
- }`
893
- );
894
- } else {
895
- console.log(
896
- `Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
897
- `${
898
- serverName
899
- ? ` (SNI: ${serverName})`
900
- : domainConfig
901
- ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
902
- : ''
903
- }`
904
- );
905
- }
906
- };
907
-
908
- // Flush all pending data to target
909
- if (record.pendingData.length > 0) {
910
- const combinedData = Buffer.concat(record.pendingData);
911
- targetSocket.write(combinedData, (err) => {
912
- if (err) {
913
- console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
914
- return this.initiateCleanupOnce(record, 'write_error');
915
- }
916
-
917
- // Establish piping now that we've flushed the buffered data
918
- setupPiping();
919
- });
920
- } else {
921
- // No pending data, just establish piping immediately
922
- setupPiping();
923
- }
924
-
925
- // Clear the buffer now that we've processed it
926
- record.pendingData = [];
927
- record.pendingDataSize = 0;
928
-
929
- // Add the renegotiation handler for SNI validation with strict domain enforcement
930
- // This will be called after we've established piping
931
- if (serverName) {
932
- // Define a handler for checking renegotiation with improved detection
933
- const renegotiationHandler = (renegChunk: Buffer) => {
934
- // Only process if this looks like a TLS ClientHello
935
- if (SniHandler.isClientHello(renegChunk)) {
936
- try {
937
- // Extract SNI from ClientHello
938
- // Create a connection info object for the existing connection
939
- const connInfo = {
940
- sourceIp: record.remoteIP,
941
- sourcePort: record.incoming.remotePort || 0,
942
- destIp: record.incoming.localAddress || '',
943
- destPort: record.incoming.localPort || 0,
944
- };
945
-
946
- // Check for session tickets if allowSessionTicket is disabled
947
- if (this.settings.allowSessionTicket === false) {
948
- // Analyze for session resumption attempt (session ticket or PSK)
949
- const resumptionInfo = SniHandler.hasSessionResumption(
950
- renegChunk,
951
- this.settings.enableTlsDebugLogging
952
- );
953
-
954
- if (resumptionInfo.isResumption) {
955
- // Always log resumption attempt for easier debugging
956
- // Try to extract SNI for logging
957
- const extractedSNI = SniHandler.extractSNI(
958
- renegChunk,
959
- this.settings.enableTlsDebugLogging
960
- );
961
- console.log(
962
- `[${connectionId}] Session resumption detected in renegotiation. ` +
963
- `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
964
- `SNI value: ${extractedSNI || 'None'}, ` +
965
- `allowSessionTicket: ${this.settings.allowSessionTicket}`
966
- );
967
-
968
- // Block if there's session resumption without SNI
969
- if (!resumptionInfo.hasSNI) {
970
- console.log(
971
- `[${connectionId}] Session resumption detected in renegotiation without SNI and allowSessionTicket=false. ` +
972
- `Terminating connection to force new TLS handshake.`
973
- );
974
- this.initiateCleanupOnce(record, 'session_ticket_blocked');
975
- return;
976
- } else {
977
- if (this.settings.enableDetailedLogging) {
978
- console.log(
979
- `[${connectionId}] Session resumption with SNI detected in renegotiation. ` +
980
- `Allowing connection since SNI is present.`
981
- );
982
- }
983
- }
984
- }
985
- }
986
-
987
- const newSNI = SniHandler.extractSNIWithResumptionSupport(
988
- renegChunk,
989
- connInfo,
990
- this.settings.enableTlsDebugLogging
991
- );
992
-
993
- // Skip if no SNI was found
994
- if (!newSNI) return;
995
-
996
- // Handle SNI change during renegotiation - always terminate for domain switches
997
- if (newSNI !== record.lockedDomain) {
998
- // Log and terminate the connection for any SNI change
999
- console.log(
1000
- `[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
1001
- `Terminating connection - SNI domain switching is not allowed.`
1002
- );
1003
- this.initiateCleanupOnce(record, 'sni_mismatch');
1004
- } else if (this.settings.enableDetailedLogging) {
1005
- console.log(
1006
- `[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
1007
- );
1008
- }
1009
- } catch (err) {
1010
- console.log(
1011
- `[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
1012
- );
1013
- }
1014
- }
1015
- };
1016
-
1017
- // Store the handler in the connection record so we can remove it during cleanup
1018
- record.renegotiationHandler = renegotiationHandler;
1019
-
1020
- // The renegotiation handler is added when piping is established
1021
- // Making it part of setupPiping ensures proper sequencing of event handlers
1022
- socket.on('data', renegotiationHandler);
1023
-
1024
- if (this.settings.enableDetailedLogging) {
1025
- console.log(
1026
- `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
1027
- );
1028
- if (this.settings.allowSessionTicket === false) {
1029
- console.log(
1030
- `[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
1031
- );
1032
- }
1033
- }
1034
- }
1035
-
1036
- // Set connection timeout with simpler logic
1037
- if (record.cleanupTimer) {
1038
- clearTimeout(record.cleanupTimer);
1039
- }
1040
-
1041
- // For immortal keep-alive connections, skip setting a timeout completely
1042
- if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
1043
- if (this.settings.enableDetailedLogging) {
1044
- console.log(
1045
- `[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
1046
- );
1047
- }
1048
- // No cleanup timer for immortal connections
1049
- }
1050
- // For extended keep-alive connections, use extended timeout
1051
- else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
1052
- const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
1053
- const safeTimeout = ensureSafeTimeout(extendedTimeout);
1054
-
1055
- record.cleanupTimer = setTimeout(() => {
1056
- console.log(
1057
- `[${connectionId}] Keep-alive connection from ${
1058
- record.remoteIP
1059
- } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
1060
- );
1061
- this.initiateCleanupOnce(record, 'extended_lifetime');
1062
- }, safeTimeout);
1063
-
1064
- // Make sure timeout doesn't keep the process alive
1065
- if (record.cleanupTimer.unref) {
1066
- record.cleanupTimer.unref();
1067
- }
1068
-
1069
- if (this.settings.enableDetailedLogging) {
1070
- console.log(
1071
- `[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
1072
- extendedTimeout
1073
- )}`
1074
- );
1075
- }
1076
- }
1077
- // For standard connections, use normal timeout
1078
- else {
1079
- // Use domain-specific timeout if available, otherwise use default
1080
- const connectionTimeout =
1081
- record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
1082
- const safeTimeout = ensureSafeTimeout(connectionTimeout);
1083
-
1084
- record.cleanupTimer = setTimeout(() => {
1085
- console.log(
1086
- `[${connectionId}] Connection from ${
1087
- record.remoteIP
1088
- } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
1089
- );
1090
- this.initiateCleanupOnce(record, 'connection_timeout');
1091
- }, safeTimeout);
1092
-
1093
- // Make sure timeout doesn't keep the process alive
1094
- if (record.cleanupTimer.unref) {
1095
- record.cleanupTimer.unref();
1096
- }
1097
- }
1098
-
1099
- // Mark TLS handshake as complete for TLS connections
1100
- if (record.isTLS) {
1101
- record.tlsHandshakeComplete = true;
1102
-
1103
- if (this.settings.enableTlsDebugLogging) {
1104
- console.log(
1105
- `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
1106
- );
1107
- }
1108
- }
1109
- });
1110
- }
1111
-
1112
- /**
1113
- * Get connections count by IP
1114
- */
1115
- private getConnectionCountByIP(ip: string): number {
1116
- return this.connectionsByIP.get(ip)?.size || 0;
1117
- }
1118
-
1119
- /**
1120
- * Check and update connection rate for an IP
1121
- */
1122
- private checkConnectionRate(ip: string): boolean {
1123
- const now = Date.now();
1124
- const minute = 60 * 1000;
1125
-
1126
- if (!this.connectionRateByIP.has(ip)) {
1127
- this.connectionRateByIP.set(ip, [now]);
1128
- return true;
1129
- }
1130
-
1131
- // Get timestamps and filter out entries older than 1 minute
1132
- const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
1133
- timestamps.push(now);
1134
- this.connectionRateByIP.set(ip, timestamps);
1135
-
1136
- // Check if rate exceeds limit
1137
- return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
1138
- }
1139
-
1140
- /**
1141
- * Track connection by IP
1142
- */
1143
- private trackConnectionByIP(ip: string, connectionId: string): void {
1144
- if (!this.connectionsByIP.has(ip)) {
1145
- this.connectionsByIP.set(ip, new Set());
1146
- }
1147
- this.connectionsByIP.get(ip)!.add(connectionId);
1148
- }
1149
-
1150
- /**
1151
- * Remove connection tracking for an IP
1152
- */
1153
- private removeConnectionByIP(ip: string, connectionId: string): void {
1154
- if (this.connectionsByIP.has(ip)) {
1155
- const connections = this.connectionsByIP.get(ip)!;
1156
- connections.delete(connectionId);
1157
- if (connections.size === 0) {
1158
- this.connectionsByIP.delete(ip);
1159
- }
1160
- }
1161
- }
1162
-
1163
- /**
1164
- * Track connection termination statistic
1165
- */
1166
- private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
1167
- this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
1168
- }
1169
-
1170
- /**
1171
- * Cleans up a connection record.
1172
- * Destroys both incoming and outgoing sockets, clears timers, and removes the record.
1173
- * @param record - The connection record to clean up
1174
- * @param reason - Optional reason for cleanup (for logging)
1175
- */
1176
- private cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
1177
- if (!record.connectionClosed) {
1178
- record.connectionClosed = true;
1179
-
1180
- // Track connection termination
1181
- this.removeConnectionByIP(record.remoteIP, record.id);
1182
-
1183
- if (record.cleanupTimer) {
1184
- clearTimeout(record.cleanupTimer);
1185
- record.cleanupTimer = undefined;
1186
- }
1187
-
1188
- // Detailed logging data
1189
- const duration = Date.now() - record.incomingStartTime;
1190
- const bytesReceived = record.bytesReceived;
1191
- const bytesSent = record.bytesSent;
1192
-
1193
- // Remove all data handlers (both standard and renegotiation) to make sure we clean up properly
1194
- if (record.incoming) {
1195
- try {
1196
- // Remove our safe data handler
1197
- record.incoming.removeAllListeners('data');
1198
-
1199
- // Reset the handler references
1200
- record.renegotiationHandler = undefined;
1201
- } catch (err) {
1202
- console.log(`[${record.id}] Error removing data handlers: ${err}`);
1203
- }
1204
- }
1205
-
1206
- try {
1207
- if (!record.incoming.destroyed) {
1208
- // Try graceful shutdown first, then force destroy after a short timeout
1209
- record.incoming.end();
1210
- const incomingTimeout = setTimeout(() => {
1211
- try {
1212
- if (record && !record.incoming.destroyed) {
1213
- record.incoming.destroy();
1214
- }
1215
- } catch (err) {
1216
- console.log(`[${record.id}] Error destroying incoming socket: ${err}`);
1217
- }
1218
- }, 1000);
1219
-
1220
- // Ensure the timeout doesn't block Node from exiting
1221
- if (incomingTimeout.unref) {
1222
- incomingTimeout.unref();
1223
- }
1224
- }
1225
- } catch (err) {
1226
- console.log(`[${record.id}] Error closing incoming socket: ${err}`);
1227
- try {
1228
- if (!record.incoming.destroyed) {
1229
- record.incoming.destroy();
1230
- }
1231
- } catch (destroyErr) {
1232
- console.log(`[${record.id}] Error destroying incoming socket: ${destroyErr}`);
1233
- }
1234
- }
1235
-
1236
- try {
1237
- if (record.outgoing && !record.outgoing.destroyed) {
1238
- // Try graceful shutdown first, then force destroy after a short timeout
1239
- record.outgoing.end();
1240
- const outgoingTimeout = setTimeout(() => {
1241
- try {
1242
- if (record && record.outgoing && !record.outgoing.destroyed) {
1243
- record.outgoing.destroy();
1244
- }
1245
- } catch (err) {
1246
- console.log(`[${record.id}] Error destroying outgoing socket: ${err}`);
1247
- }
1248
- }, 1000);
1249
-
1250
- // Ensure the timeout doesn't block Node from exiting
1251
- if (outgoingTimeout.unref) {
1252
- outgoingTimeout.unref();
1253
- }
1254
- }
1255
- } catch (err) {
1256
- console.log(`[${record.id}] Error closing outgoing socket: ${err}`);
1257
- try {
1258
- if (record.outgoing && !record.outgoing.destroyed) {
1259
- record.outgoing.destroy();
1260
- }
1261
- } catch (destroyErr) {
1262
- console.log(`[${record.id}] Error destroying outgoing socket: ${destroyErr}`);
1263
- }
1264
- }
1265
-
1266
- // Clear pendingData to avoid memory leaks
1267
- record.pendingData = [];
1268
- record.pendingDataSize = 0;
1269
-
1270
- // Remove the record from the tracking map
1271
- this.connectionRecords.delete(record.id);
1272
-
1273
- // Log connection details
1274
- if (this.settings.enableDetailedLogging) {
1275
- console.log(
1276
- `[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
1277
- ` Duration: ${plugins.prettyMs(
1278
- duration
1279
- )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
1280
- `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1281
- record.hasKeepAlive ? 'Yes' : 'No'
1282
- }` +
1283
- `${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
1284
- `${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
1285
- );
1286
- } else {
1287
- console.log(
1288
- `[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`
1289
- );
1290
- }
1291
- }
1292
- }
1293
-
1294
- /**
1295
- * Update connection activity timestamp
1296
- */
1297
- private updateActivity(record: IConnectionRecord): void {
1298
- record.lastActivity = Date.now();
1299
-
1300
- // Clear any inactivity warning
1301
- if (record.inactivityWarningIssued) {
1302
- record.inactivityWarningIssued = false;
1303
- }
1304
- }
1305
-
1306
- /**
1307
- * Get target IP with round-robin support
1308
- */
1309
- private getTargetIP(domainConfig: IDomainConfig): string {
1310
- if (domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
1311
- const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
1312
- const ip = domainConfig.targetIPs[currentIndex % domainConfig.targetIPs.length];
1313
- this.domainTargetIndices.set(domainConfig, currentIndex + 1);
1314
- return ip;
1315
- }
1316
- return this.settings.targetIP!;
1317
- }
1318
-
1319
- /**
1320
- * Initiates cleanup once for a connection
1321
- */
1322
- private initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
1323
- if (this.settings.enableDetailedLogging) {
1324
- console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
1325
- }
1326
-
1327
- if (
1328
- record.incomingTerminationReason === null ||
1329
- record.incomingTerminationReason === undefined
1330
- ) {
1331
- record.incomingTerminationReason = reason;
1332
- this.incrementTerminationStat('incoming', reason);
1333
- }
1334
-
1335
- this.cleanupConnection(record, reason);
1336
- }
1337
-
1338
- /**
1339
- * Creates a generic error handler for incoming or outgoing sockets
1340
- */
1341
- private handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
1342
- return (err: Error) => {
1343
- const code = (err as any).code;
1344
- let reason = 'error';
1345
-
1346
- const now = Date.now();
1347
- const connectionDuration = now - record.incomingStartTime;
1348
- const lastActivityAge = now - record.lastActivity;
1349
-
1350
- if (code === 'ECONNRESET') {
1351
- reason = 'econnreset';
1352
- console.log(
1353
- `[${record.id}] ECONNRESET on ${side} side from ${record.remoteIP}: ${
1354
- err.message
1355
- }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
1356
- lastActivityAge
1357
- )} ago`
1358
- );
1359
- } else if (code === 'ETIMEDOUT') {
1360
- reason = 'etimedout';
1361
- console.log(
1362
- `[${record.id}] ETIMEDOUT on ${side} side from ${record.remoteIP}: ${
1363
- err.message
1364
- }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
1365
- lastActivityAge
1366
- )} ago`
1367
- );
1368
- } else {
1369
- console.log(
1370
- `[${record.id}] Error on ${side} side from ${record.remoteIP}: ${
1371
- err.message
1372
- }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
1373
- lastActivityAge
1374
- )} ago`
1375
- );
1376
- }
1377
-
1378
- if (side === 'incoming' && record.incomingTerminationReason === null) {
1379
- record.incomingTerminationReason = reason;
1380
- this.incrementTerminationStat('incoming', reason);
1381
- } else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
1382
- record.outgoingTerminationReason = reason;
1383
- this.incrementTerminationStat('outgoing', reason);
1384
- }
1385
-
1386
- this.initiateCleanupOnce(record, reason);
1387
- };
1388
- }
1389
-
1390
- /**
1391
- * Creates a generic close handler for incoming or outgoing sockets
1392
- */
1393
- private handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
1394
- return () => {
1395
- if (this.settings.enableDetailedLogging) {
1396
- console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`);
1397
- }
1398
-
1399
- if (side === 'incoming' && record.incomingTerminationReason === null) {
1400
- record.incomingTerminationReason = 'normal';
1401
- this.incrementTerminationStat('incoming', 'normal');
1402
- } else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
1403
- record.outgoingTerminationReason = 'normal';
1404
- this.incrementTerminationStat('outgoing', 'normal');
1405
- // Record the time when outgoing socket closed.
1406
- record.outgoingClosedTime = Date.now();
1407
- }
1408
-
1409
- this.initiateCleanupOnce(record, 'closed_' + side);
1410
- };
1411
- }
1412
-
1413
- /**
1414
- * Main method to start the proxy
1415
- */
1416
- public async start() {
1417
- // Don't start if already shutting down
1418
- if (this.isShuttingDown) {
1419
- console.log("Cannot start PortProxy while it's shutting down");
1420
- return;
1421
- }
1422
-
1423
- // Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized)
1424
- if (
1425
- this.settings.useNetworkProxy &&
1426
- this.settings.useNetworkProxy.length > 0 &&
1427
- !this.networkProxy
1428
- ) {
1429
- await this.initializeNetworkProxy();
1430
- }
1431
-
1432
- // Start NetworkProxy if configured
1433
- if (this.networkProxy) {
1434
- await this.networkProxy.start();
1435
- console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
1436
-
1437
- // Log ACME status
1438
- if (this.settings.acme?.enabled) {
1439
- console.log(
1440
- `ACME certificate management is enabled (${
1441
- this.settings.acme.useProduction ? 'Production' : 'Staging'
1442
- } mode)`
1443
- );
1444
- console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`);
1445
-
1446
- // Register domains for ACME certificates if enabled
1447
- if (this.networkProxy.options.acme?.enabled) {
1448
- console.log('Registering domains with ACME certificate manager...');
1449
- // The NetworkProxy will handle this internally via registerDomainsWithAcmeManager()
1450
- }
1451
- }
1452
- }
1453
-
1454
- // Define a unified connection handler for all listening ports.
1455
- const connectionHandler = (socket: plugins.net.Socket) => {
1456
- if (this.isShuttingDown) {
1457
- socket.end();
1458
- socket.destroy();
1459
- return;
1460
- }
1461
-
1462
- const remoteIP = socket.remoteAddress || '';
1463
- const localPort = socket.localPort || 0; // The port on which this connection was accepted.
1464
-
1465
- // Check rate limits
1466
- if (
1467
- this.settings.maxConnectionsPerIP &&
1468
- this.getConnectionCountByIP(remoteIP) >= this.settings.maxConnectionsPerIP
1469
- ) {
1470
- console.log(
1471
- `Connection rejected from ${remoteIP}: Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`
1472
- );
1473
- socket.end();
1474
- socket.destroy();
1475
- return;
1476
- }
1477
-
1478
- if (this.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(remoteIP)) {
1479
- console.log(
1480
- `Connection rejected from ${remoteIP}: Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`
1481
- );
1482
- socket.end();
1483
- socket.destroy();
1484
- return;
1485
- }
1486
-
1487
- // Apply socket optimizations
1488
- socket.setNoDelay(this.settings.noDelay);
1489
-
1490
- // Create a unique connection ID and record
1491
- const connectionId = generateConnectionId();
1492
- const connectionRecord: IConnectionRecord = {
1493
- id: connectionId,
1494
- incoming: socket,
1495
- outgoing: null,
1496
- incomingStartTime: Date.now(),
1497
- lastActivity: Date.now(),
1498
- connectionClosed: false,
1499
- pendingData: [],
1500
- pendingDataSize: 0,
1501
-
1502
- // Initialize enhanced tracking fields
1503
- bytesReceived: 0,
1504
- bytesSent: 0,
1505
- remoteIP: remoteIP,
1506
- localPort: localPort,
1507
- isTLS: false,
1508
- tlsHandshakeComplete: false,
1509
- hasReceivedInitialData: false,
1510
- hasKeepAlive: false, // Will set to true if keep-alive is applied
1511
- incomingTerminationReason: null,
1512
- outgoingTerminationReason: null,
1513
-
1514
- // Initialize NetworkProxy tracking
1515
- usingNetworkProxy: false,
1516
-
1517
- // Initialize browser connection tracking
1518
- isBrowserConnection: false,
1519
- domainSwitches: 0,
1520
- };
1521
-
1522
- // Apply keep-alive settings if enabled
1523
- if (this.settings.keepAlive) {
1524
- socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
1525
- connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
1526
-
1527
- // Apply enhanced TCP keep-alive options if enabled
1528
- if (this.settings.enableKeepAliveProbes) {
1529
- try {
1530
- // These are platform-specific and may not be available
1531
- if ('setKeepAliveProbes' in socket) {
1532
- (socket as any).setKeepAliveProbes(10); // More aggressive probing
1533
- }
1534
- if ('setKeepAliveInterval' in socket) {
1535
- (socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
1536
- }
1537
- } catch (err) {
1538
- // Ignore errors - these are optional enhancements
1539
- if (this.settings.enableDetailedLogging) {
1540
- console.log(
1541
- `[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
1542
- );
1543
- }
1544
- }
1545
- }
1546
- }
1547
-
1548
- // Track connection by IP
1549
- this.trackConnectionByIP(remoteIP, connectionId);
1550
- this.connectionRecords.set(connectionId, connectionRecord);
1551
-
1552
- if (this.settings.enableDetailedLogging) {
1553
- console.log(
1554
- `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
1555
- `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
1556
- `Active connections: ${this.connectionRecords.size}`
1557
- );
1558
- } else {
1559
- console.log(
1560
- `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`
1561
- );
1562
- }
1563
-
1564
- // Check if this connection should be forwarded directly to NetworkProxy
1565
- // First check port-based forwarding settings
1566
- let shouldUseNetworkProxy =
1567
- this.settings.useNetworkProxy && this.settings.useNetworkProxy.includes(localPort);
1568
-
1569
- // We'll look for domain-specific settings after SNI extraction
1570
-
1571
- if (shouldUseNetworkProxy) {
1572
- // For NetworkProxy ports, we want to capture the TLS handshake and forward directly
1573
- let initialDataReceived = false;
1574
-
1575
- // Set an initial timeout for handshake data
1576
- let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
1577
- if (!initialDataReceived) {
1578
- console.log(
1579
- `[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
1580
- );
1581
- if (connectionRecord.incomingTerminationReason === null) {
1582
- connectionRecord.incomingTerminationReason = 'initial_timeout';
1583
- this.incrementTerminationStat('incoming', 'initial_timeout');
1584
- }
1585
- socket.end();
1586
- this.cleanupConnection(connectionRecord, 'initial_timeout');
1587
- }
1588
- }, this.settings.initialDataTimeout!);
1589
-
1590
- // Make sure timeout doesn't keep the process alive
1591
- if (initialTimeout.unref) {
1592
- initialTimeout.unref();
1593
- }
1594
-
1595
- socket.on('error', this.handleError('incoming', connectionRecord));
1596
-
1597
- // First data handler to capture initial TLS handshake for NetworkProxy
1598
- socket.once('data', (chunk: Buffer) => {
1599
- // Clear the initial timeout since we've received data
1600
- if (initialTimeout) {
1601
- clearTimeout(initialTimeout);
1602
- initialTimeout = null;
1603
- }
1604
-
1605
- initialDataReceived = true;
1606
- connectionRecord.hasReceivedInitialData = true;
1607
-
1608
- // Block non-TLS connections on port 443
1609
- // Always enforce TLS on standard HTTPS port
1610
- if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) {
1611
- console.log(
1612
- `[${connectionId}] Non-TLS connection detected on port 443. ` +
1613
- `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
1614
- );
1615
- if (connectionRecord.incomingTerminationReason === null) {
1616
- connectionRecord.incomingTerminationReason = 'non_tls_blocked';
1617
- this.incrementTerminationStat('incoming', 'non_tls_blocked');
1618
- }
1619
- socket.end();
1620
- this.cleanupConnection(connectionRecord, 'non_tls_blocked');
1621
- return;
1622
- }
1623
-
1624
- // Check if this looks like a TLS handshake
1625
- if (SniHandler.isTlsHandshake(chunk)) {
1626
- connectionRecord.isTLS = true;
1627
-
1628
- // Check for TLS ClientHello with either no SNI or session tickets
1629
- if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
1630
- // Extract SNI first
1631
- const extractedSNI = SniHandler.extractSNI(
1632
- chunk,
1633
- this.settings.enableTlsDebugLogging
1634
- );
1635
- const hasSNI = !!extractedSNI;
1636
-
1637
- // Analyze for session resumption attempt
1638
- const resumptionInfo = SniHandler.hasSessionResumption(
1639
- chunk,
1640
- this.settings.enableTlsDebugLogging
1641
- );
1642
-
1643
- // Always log for debugging purposes
1644
- console.log(
1645
- `[${connectionId}] TLS ClientHello detected with allowSessionTicket=false. ` +
1646
- `Has SNI: ${hasSNI ? 'Yes' : 'No'}, ` +
1647
- `SNI value: ${extractedSNI || 'None'}, ` +
1648
- `Has session resumption: ${resumptionInfo.isResumption ? 'Yes' : 'No'}`
1649
- );
1650
-
1651
- // Block if this is a connection with session resumption but no SNI
1652
- if (resumptionInfo.isResumption && !hasSNI) {
1653
- console.log(
1654
- `[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
1655
- `Terminating connection to force new TLS handshake.`
1656
- );
1657
- if (connectionRecord.incomingTerminationReason === null) {
1658
- connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
1659
- this.incrementTerminationStat('incoming', 'session_ticket_blocked');
1660
- }
1661
- socket.end();
1662
- this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
1663
- return;
1664
- }
1665
-
1666
- // Also block if this is a TLS connection without SNI when allowSessionTicket is false
1667
- // This forces clients to send SNI which helps with routing
1668
- if (!hasSNI && localPort === 443) {
1669
- console.log(
1670
- `[${connectionId}] TLS ClientHello detected on port 443 without SNI and allowSessionTicket=false. ` +
1671
- `Terminating connection to force proper SNI in handshake.`
1672
- );
1673
- if (connectionRecord.incomingTerminationReason === null) {
1674
- connectionRecord.incomingTerminationReason = 'no_sni_blocked';
1675
- this.incrementTerminationStat('incoming', 'no_sni_blocked');
1676
- }
1677
- socket.end();
1678
- this.cleanupConnection(connectionRecord, 'no_sni_blocked');
1679
- return;
1680
- }
1681
- }
1682
-
1683
- // Try to extract SNI for domain-specific NetworkProxy handling
1684
- const connInfo = {
1685
- sourceIp: remoteIP,
1686
- sourcePort: socket.remotePort || 0,
1687
- destIp: socket.localAddress || '',
1688
- destPort: socket.localPort || 0,
1689
- };
1690
-
1691
- // Extract SNI to check for domain-specific NetworkProxy settings
1692
- const serverName = SniHandler.processTlsPacket(
1693
- chunk,
1694
- connInfo,
1695
- this.settings.enableTlsDebugLogging
1696
- );
1697
-
1698
- if (serverName) {
1699
- // If we got an SNI, check for domain-specific NetworkProxy settings
1700
- const domainConfig = this.settings.domainConfigs.find((config) =>
1701
- config.domains.some((d) => plugins.minimatch(serverName, d))
1702
- );
1703
-
1704
- // Save domain config and SNI in connection record
1705
- connectionRecord.domainConfig = domainConfig;
1706
- connectionRecord.lockedDomain = serverName;
1707
-
1708
- // Use domain-specific NetworkProxy port if configured
1709
- if (domainConfig?.useNetworkProxy) {
1710
- const networkProxyPort =
1711
- domainConfig.networkProxyPort || this.settings.networkProxyPort;
1712
-
1713
- if (this.settings.enableDetailedLogging) {
1714
- console.log(
1715
- `[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
1716
- );
1717
- }
1718
-
1719
- // Forward to NetworkProxy with domain-specific port
1720
- this.forwardToNetworkProxy(
1721
- connectionId,
1722
- socket,
1723
- connectionRecord,
1724
- chunk,
1725
- networkProxyPort
1726
- );
1727
- return;
1728
- }
1729
- }
1730
-
1731
- // Forward directly to NetworkProxy without domain-specific settings
1732
- this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
1733
- } else {
1734
- // If not TLS, use normal direct connection
1735
- console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
1736
- this.setupDirectConnection(
1737
- connectionId,
1738
- socket,
1739
- connectionRecord,
1740
- undefined,
1741
- undefined,
1742
- chunk
1743
- );
1744
- }
1745
- });
1746
- } else {
1747
- // For non-NetworkProxy ports, proceed with normal processing
1748
-
1749
- // Define helpers for rejecting connections
1750
- const rejectIncomingConnection = (reason: string, logMessage: string) => {
1751
- console.log(`[${connectionId}] ${logMessage}`);
1752
- socket.end();
1753
- if (connectionRecord.incomingTerminationReason === null) {
1754
- connectionRecord.incomingTerminationReason = reason;
1755
- this.incrementTerminationStat('incoming', reason);
1756
- }
1757
- this.cleanupConnection(connectionRecord, reason);
1758
- };
1759
-
1760
- let initialDataReceived = false;
1761
-
1762
- // Set an initial timeout for SNI data if needed
1763
- let initialTimeout: NodeJS.Timeout | null = null;
1764
- if (this.settings.sniEnabled) {
1765
- initialTimeout = setTimeout(() => {
1766
- if (!initialDataReceived) {
1767
- console.log(
1768
- `[${connectionId}] Initial data timeout (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP} on port ${localPort}`
1769
- );
1770
- if (connectionRecord.incomingTerminationReason === null) {
1771
- connectionRecord.incomingTerminationReason = 'initial_timeout';
1772
- this.incrementTerminationStat('incoming', 'initial_timeout');
1773
- }
1774
- socket.end();
1775
- this.cleanupConnection(connectionRecord, 'initial_timeout');
1776
- }
1777
- }, this.settings.initialDataTimeout!);
1778
-
1779
- // Make sure timeout doesn't keep the process alive
1780
- if (initialTimeout.unref) {
1781
- initialTimeout.unref();
1782
- }
1783
- } else {
1784
- initialDataReceived = true;
1785
- connectionRecord.hasReceivedInitialData = true;
1786
- }
1787
-
1788
- socket.on('error', this.handleError('incoming', connectionRecord));
1789
-
1790
- // Track data for bytes counting
1791
- socket.on('data', (chunk: Buffer) => {
1792
- connectionRecord.bytesReceived += chunk.length;
1793
- this.updateActivity(connectionRecord);
1794
-
1795
- // Check for TLS handshake if this is the first chunk
1796
- if (!connectionRecord.isTLS && SniHandler.isTlsHandshake(chunk)) {
1797
- connectionRecord.isTLS = true;
1798
-
1799
- if (this.settings.enableTlsDebugLogging) {
1800
- console.log(
1801
- `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
1802
- );
1803
- // Try to extract SNI and log detailed debug info
1804
- // Create connection info for debug logging
1805
- const debugConnInfo = {
1806
- sourceIp: remoteIP,
1807
- sourcePort: socket.remotePort || 0,
1808
- destIp: socket.localAddress || '',
1809
- destPort: socket.localPort || 0,
1810
- };
1811
-
1812
- SniHandler.extractSNIWithResumptionSupport(chunk, debugConnInfo, true);
1813
- }
1814
- }
1815
- });
1816
-
1817
- /**
1818
- * Sets up the connection to the target host.
1819
- * @param serverName - The SNI hostname (unused when forcedDomain is provided).
1820
- * @param initialChunk - Optional initial data chunk.
1821
- * @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
1822
- * @param overridePort - If provided, use this port for the outgoing connection.
1823
- */
1824
- const setupConnection = (
1825
- serverName: string,
1826
- initialChunk?: Buffer,
1827
- forcedDomain?: IDomainConfig,
1828
- overridePort?: number
1829
- ) => {
1830
- // Clear the initial timeout since we've received data
1831
- if (initialTimeout) {
1832
- clearTimeout(initialTimeout);
1833
- initialTimeout = null;
1834
- }
1835
-
1836
- // Mark that we've received initial data
1837
- initialDataReceived = true;
1838
- connectionRecord.hasReceivedInitialData = true;
1839
-
1840
- // Check if this looks like a TLS handshake
1841
- const isTlsHandshakeDetected = initialChunk && SniHandler.isTlsHandshake(initialChunk);
1842
- if (isTlsHandshakeDetected) {
1843
- connectionRecord.isTLS = true;
1844
-
1845
- if (this.settings.enableTlsDebugLogging) {
1846
- console.log(
1847
- `[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
1848
- );
1849
- }
1850
- }
1851
-
1852
- // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
1853
- const domainConfig = forcedDomain
1854
- ? forcedDomain
1855
- : serverName
1856
- ? this.settings.domainConfigs.find((config) =>
1857
- config.domains.some((d) => plugins.minimatch(serverName, d))
1858
- )
1859
- : undefined;
1860
-
1861
- // Save domain config in connection record
1862
- connectionRecord.domainConfig = domainConfig;
1863
-
1864
- // Check if this domain should use NetworkProxy (domain-specific setting)
1865
- if (domainConfig?.useNetworkProxy && this.networkProxy) {
1866
- if (this.settings.enableDetailedLogging) {
1867
- console.log(
1868
- `[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`
1869
- );
1870
- }
1871
-
1872
- const networkProxyPort =
1873
- domainConfig.networkProxyPort || this.settings.networkProxyPort;
1874
-
1875
- if (initialChunk && connectionRecord.isTLS) {
1876
- // For TLS connections with initial chunk, forward to NetworkProxy
1877
- this.forwardToNetworkProxy(
1878
- connectionId,
1879
- socket,
1880
- connectionRecord,
1881
- initialChunk,
1882
- networkProxyPort // Pass the domain-specific NetworkProxy port if configured
1883
- );
1884
- return; // Skip normal connection setup
1885
- }
1886
- }
1887
-
1888
- // IP validation is skipped if allowedIPs is empty
1889
- if (domainConfig) {
1890
- const effectiveAllowedIPs: string[] = [
1891
- ...domainConfig.allowedIPs,
1892
- ...(this.settings.defaultAllowedIPs || []),
1893
- ];
1894
- const effectiveBlockedIPs: string[] = [
1895
- ...(domainConfig.blockedIPs || []),
1896
- ...(this.settings.defaultBlockedIPs || []),
1897
- ];
1898
-
1899
- // Skip IP validation if allowedIPs is empty
1900
- if (
1901
- domainConfig.allowedIPs.length > 0 &&
1902
- !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)
1903
- ) {
1904
- return rejectIncomingConnection(
1905
- 'rejected',
1906
- `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(
1907
- ', '
1908
- )}`
1909
- );
1910
- }
1911
- } else if (
1912
- this.settings.defaultAllowedIPs &&
1913
- this.settings.defaultAllowedIPs.length > 0
1914
- ) {
1915
- if (
1916
- !isGlobIPAllowed(
1917
- remoteIP,
1918
- this.settings.defaultAllowedIPs,
1919
- this.settings.defaultBlockedIPs || []
1920
- )
1921
- ) {
1922
- return rejectIncomingConnection(
1923
- 'rejected',
1924
- `Connection rejected: IP ${remoteIP} not allowed by default allowed list`
1925
- );
1926
- }
1927
- }
1928
-
1929
- // Save the initial SNI
1930
- if (serverName) {
1931
- connectionRecord.lockedDomain = serverName;
1932
- }
1933
-
1934
- // Set up the direct connection
1935
- return this.setupDirectConnection(
1936
- connectionId,
1937
- socket,
1938
- connectionRecord,
1939
- domainConfig,
1940
- serverName,
1941
- initialChunk,
1942
- overridePort
1943
- );
1944
- };
1945
-
1946
- // --- PORT RANGE-BASED HANDLING ---
1947
- // Only apply port-based rules if the incoming port is within one of the global port ranges.
1948
- if (
1949
- this.settings.globalPortRanges &&
1950
- isPortInRanges(localPort, this.settings.globalPortRanges)
1951
- ) {
1952
- if (this.settings.forwardAllGlobalRanges) {
1953
- if (
1954
- this.settings.defaultAllowedIPs &&
1955
- this.settings.defaultAllowedIPs.length > 0 &&
1956
- !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
1957
- ) {
1958
- console.log(
1959
- `[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`
1960
- );
1961
- socket.end();
1962
- return;
1963
- }
1964
- if (this.settings.enableDetailedLogging) {
1965
- console.log(
1966
- `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
1967
- );
1968
- }
1969
- setupConnection(
1970
- '',
1971
- undefined,
1972
- {
1973
- domains: ['global'],
1974
- allowedIPs: this.settings.defaultAllowedIPs || [],
1975
- blockedIPs: this.settings.defaultBlockedIPs || [],
1976
- targetIPs: [this.settings.targetIP!],
1977
- portRanges: [],
1978
- },
1979
- localPort
1980
- );
1981
- return;
1982
- } else {
1983
- // Attempt to find a matching forced domain config based on the local port.
1984
- const forcedDomain = this.settings.domainConfigs.find(
1985
- (domain) =>
1986
- domain.portRanges &&
1987
- domain.portRanges.length > 0 &&
1988
- isPortInRanges(localPort, domain.portRanges)
1989
- );
1990
- if (forcedDomain) {
1991
- const effectiveAllowedIPs: string[] = [
1992
- ...forcedDomain.allowedIPs,
1993
- ...(this.settings.defaultAllowedIPs || []),
1994
- ];
1995
- const effectiveBlockedIPs: string[] = [
1996
- ...(forcedDomain.blockedIPs || []),
1997
- ...(this.settings.defaultBlockedIPs || []),
1998
- ];
1999
- if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
2000
- console.log(
2001
- `[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
2002
- ', '
2003
- )} on port ${localPort}.`
2004
- );
2005
- socket.end();
2006
- return;
2007
- }
2008
- if (this.settings.enableDetailedLogging) {
2009
- console.log(
2010
- `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
2011
- ', '
2012
- )}.`
2013
- );
2014
- }
2015
- setupConnection('', undefined, forcedDomain, localPort);
2016
- return;
2017
- }
2018
- // Fall through to SNI/default handling if no forced domain config is found.
2019
- }
2020
- }
2021
-
2022
- // --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
2023
- if (this.settings.sniEnabled) {
2024
- initialDataReceived = false;
2025
-
2026
- socket.once('data', (chunk: Buffer) => {
2027
- if (initialTimeout) {
2028
- clearTimeout(initialTimeout);
2029
- initialTimeout = null;
2030
- }
2031
-
2032
- initialDataReceived = true;
2033
-
2034
- // ADD THE DEBUGGING CODE RIGHT HERE, BEFORE ANY OTHER PROCESSING
2035
- if (SniHandler.isClientHello(chunk)) {
2036
- // Log more details to understand session resumption
2037
- const resumptionInfo = SniHandler.hasSessionResumption(chunk, true);
2038
- console.log(
2039
- `[${connectionId}] ClientHello details: isResumption=${resumptionInfo.isResumption}, hasSNI=${resumptionInfo.hasSNI}`
2040
- );
2041
-
2042
- // Try both extraction methods
2043
- const standardSNI = SniHandler.extractSNI(chunk, true);
2044
- const pskSNI = SniHandler.extractSNIFromPSKExtension(chunk, true);
2045
-
2046
- console.log(
2047
- `[${connectionId}] SNI extraction results: standardSNI=${
2048
- standardSNI || 'none'
2049
- }, pskSNI=${pskSNI || 'none'}`
2050
- );
2051
- }
2052
-
2053
- // Block non-TLS connections on port 443
2054
- // Always enforce TLS on standard HTTPS port
2055
- if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) {
2056
- console.log(
2057
- `[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
2058
- `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
2059
- );
2060
- if (connectionRecord.incomingTerminationReason === null) {
2061
- connectionRecord.incomingTerminationReason = 'non_tls_blocked';
2062
- this.incrementTerminationStat('incoming', 'non_tls_blocked');
2063
- }
2064
- socket.end();
2065
- this.cleanupConnection(connectionRecord, 'non_tls_blocked');
2066
- return;
2067
- }
2068
-
2069
- // Try to extract SNI
2070
- let serverName = '';
2071
-
2072
- if (SniHandler.isTlsHandshake(chunk)) {
2073
- connectionRecord.isTLS = true;
2074
-
2075
- if (this.settings.enableTlsDebugLogging) {
2076
- console.log(
2077
- `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
2078
- );
2079
- }
2080
-
2081
- // Check for session tickets if allowSessionTicket is disabled
2082
- if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
2083
- // Analyze for session resumption attempt
2084
- const resumptionInfo = SniHandler.hasSessionResumption(
2085
- chunk,
2086
- this.settings.enableTlsDebugLogging
2087
- );
2088
-
2089
- if (resumptionInfo.isResumption) {
2090
- // Always log resumption attempt for easier debugging
2091
- // Try to extract SNI for logging
2092
- const extractedSNI = SniHandler.extractSNI(
2093
- chunk,
2094
- this.settings.enableTlsDebugLogging
2095
- );
2096
- console.log(
2097
- `[${connectionId}] Session resumption detected in SNI handler. ` +
2098
- `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
2099
- `SNI value: ${extractedSNI || 'None'}, ` +
2100
- `allowSessionTicket: ${this.settings.allowSessionTicket}`
2101
- );
2102
-
2103
- // Block if there's session resumption without SNI
2104
- if (!resumptionInfo.hasSNI) {
2105
- console.log(
2106
- `[${connectionId}] Session resumption detected in SNI handler without SNI and allowSessionTicket=false. ` +
2107
- `Terminating connection to force new TLS handshake.`
2108
- );
2109
- if (connectionRecord.incomingTerminationReason === null) {
2110
- connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
2111
- this.incrementTerminationStat('incoming', 'session_ticket_blocked');
2112
- }
2113
- socket.end();
2114
- this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
2115
- return;
2116
- } else {
2117
- if (this.settings.enableDetailedLogging) {
2118
- console.log(
2119
- `[${connectionId}] Session resumption with SNI detected in SNI handler. ` +
2120
- `Allowing connection since SNI is present.`
2121
- );
2122
- }
2123
- }
2124
- }
2125
- }
2126
-
2127
- // Create connection info object for SNI extraction
2128
- const connInfo = {
2129
- sourceIp: remoteIP,
2130
- sourcePort: socket.remotePort || 0,
2131
- destIp: socket.localAddress || '',
2132
- destPort: socket.localPort || 0,
2133
- };
2134
-
2135
- // Use the new processTlsPacket method for comprehensive handling
2136
- serverName =
2137
- SniHandler.processTlsPacket(
2138
- chunk,
2139
- connInfo,
2140
- this.settings.enableTlsDebugLogging,
2141
- connectionRecord.lockedDomain // Pass any previously negotiated domain as a hint
2142
- ) || '';
2143
- }
2144
-
2145
- // Lock the connection to the negotiated SNI.
2146
- connectionRecord.lockedDomain = serverName;
2147
-
2148
- if (this.settings.enableDetailedLogging) {
2149
- console.log(
2150
- `[${connectionId}] Received connection from ${remoteIP} with SNI: ${
2151
- serverName || '(empty)'
2152
- }`
2153
- );
2154
- }
2155
-
2156
- setupConnection(serverName, chunk);
2157
- });
2158
- } else {
2159
- initialDataReceived = true;
2160
- connectionRecord.hasReceivedInitialData = true;
2161
-
2162
- if (
2163
- this.settings.defaultAllowedIPs &&
2164
- this.settings.defaultAllowedIPs.length > 0 &&
2165
- !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
2166
- ) {
2167
- return rejectIncomingConnection(
2168
- 'rejected',
2169
- `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`
2170
- );
2171
- }
2172
-
2173
- setupConnection('');
2174
- }
2175
- }
2176
- };
2177
-
2178
- // --- SETUP LISTENERS ---
2179
- // Determine which ports to listen on.
2180
- const listeningPorts = new Set<number>();
2181
- if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
2182
- // Listen on every port defined by the global ranges.
2183
- for (const range of this.settings.globalPortRanges) {
2184
- for (let port = range.from; port <= range.to; port++) {
2185
- listeningPorts.add(port);
2186
- }
2187
- }
2188
- // Also ensure the default fromPort is listened to if it isn't already in the ranges.
2189
- listeningPorts.add(this.settings.fromPort);
2190
- } else {
2191
- listeningPorts.add(this.settings.fromPort);
2192
- }
2193
-
2194
- // Create a server for each port.
2195
- for (const port of listeningPorts) {
2196
- const server = plugins.net.createServer(connectionHandler).on('error', (err: Error) => {
2197
- console.log(`Server Error on port ${port}: ${err.message}`);
2198
- });
2199
- server.listen(port, () => {
2200
- const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
2201
- console.log(
2202
- `PortProxy -> OK: Now listening on port ${port}${
2203
- this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
2204
- }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
2205
- );
2206
- });
2207
- this.netServers.push(server);
2208
- }
2209
-
2210
- // Log active connection count, longest running durations, and run parity checks periodically
2211
- this.connectionLogger = setInterval(() => {
2212
- // Immediately return if shutting down
2213
- if (this.isShuttingDown) return;
2214
-
2215
- const now = Date.now();
2216
- let maxIncoming = 0;
2217
- let maxOutgoing = 0;
2218
- let tlsConnections = 0;
2219
- let nonTlsConnections = 0;
2220
- let completedTlsHandshakes = 0;
2221
- let pendingTlsHandshakes = 0;
2222
- let keepAliveConnections = 0;
2223
- let networkProxyConnections = 0;
2224
- let domainSwitchedConnections = 0;
2225
-
2226
- // Create a copy of the keys to avoid modification during iteration
2227
- const connectionIds = [...this.connectionRecords.keys()];
2228
-
2229
- for (const id of connectionIds) {
2230
- const record = this.connectionRecords.get(id);
2231
- if (!record) continue;
2232
-
2233
- // Track connection stats
2234
- if (record.isTLS) {
2235
- tlsConnections++;
2236
- if (record.tlsHandshakeComplete) {
2237
- completedTlsHandshakes++;
2238
- } else {
2239
- pendingTlsHandshakes++;
2240
- }
2241
- } else {
2242
- nonTlsConnections++;
2243
- }
2244
-
2245
- if (record.hasKeepAlive) {
2246
- keepAliveConnections++;
2247
- }
2248
-
2249
- if (record.usingNetworkProxy) {
2250
- networkProxyConnections++;
2251
- }
2252
-
2253
- if (record.domainSwitches && record.domainSwitches > 0) {
2254
- domainSwitchedConnections++;
2255
- }
2256
-
2257
- maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
2258
- if (record.outgoingStartTime) {
2259
- maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
2260
- }
2261
- // Parity check: if outgoing socket closed and incoming remains active
2262
- if (
2263
- record.outgoingClosedTime &&
2264
- !record.incoming.destroyed &&
2265
- !record.connectionClosed &&
2266
- now - record.outgoingClosedTime > 120000
2267
- ) {
2268
- const remoteIP = record.remoteIP;
2269
- console.log(
2270
- `[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(
2271
- now - record.outgoingClosedTime
2272
- )} after outgoing closed.`
2273
- );
2274
- this.cleanupConnection(record, 'parity_check');
2275
- }
2276
-
2277
- // Check for stalled connections waiting for initial data
2278
- if (
2279
- !record.hasReceivedInitialData &&
2280
- now - record.incomingStartTime > this.settings.initialDataTimeout! / 2
2281
- ) {
2282
- console.log(
2283
- `[${id}] Warning: Connection from ${
2284
- record.remoteIP
2285
- } has not received initial data after ${plugins.prettyMs(
2286
- now - record.incomingStartTime
2287
- )}`
2288
- );
2289
- }
2290
-
2291
- // Skip inactivity check if disabled or for immortal keep-alive connections
2292
- if (
2293
- !this.settings.disableInactivityCheck &&
2294
- !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
2295
- ) {
2296
- const inactivityTime = now - record.lastActivity;
2297
-
2298
- // Use extended timeout for extended-treatment keep-alive connections
2299
- let effectiveTimeout = this.settings.inactivityTimeout!;
2300
- if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
2301
- const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
2302
- effectiveTimeout = effectiveTimeout * multiplier;
2303
- }
2304
-
2305
- if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
2306
- // For keep-alive connections, issue a warning first
2307
- if (record.hasKeepAlive && !record.inactivityWarningIssued) {
2308
- console.log(
2309
- `[${id}] Warning: Keep-alive connection from ${
2310
- record.remoteIP
2311
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
2312
- `Will close in 10 minutes if no activity.`
2313
- );
2314
-
2315
- // Set warning flag and add grace period
2316
- record.inactivityWarningIssued = true;
2317
- record.lastActivity = now - (effectiveTimeout - 600000);
2318
-
2319
- // Try to stimulate activity with a probe packet
2320
- if (record.outgoing && !record.outgoing.destroyed) {
2321
- try {
2322
- record.outgoing.write(Buffer.alloc(0));
2323
-
2324
- if (this.settings.enableDetailedLogging) {
2325
- console.log(`[${id}] Sent probe packet to test keep-alive connection`);
2326
- }
2327
- } catch (err) {
2328
- console.log(`[${id}] Error sending probe packet: ${err}`);
2329
- }
2330
- }
2331
- } else {
2332
- // For non-keep-alive or after warning, close the connection
2333
- console.log(
2334
- `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
2335
- `for ${plugins.prettyMs(inactivityTime)}.` +
2336
- (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
2337
- );
2338
- this.cleanupConnection(record, 'inactivity');
2339
- }
2340
- } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
2341
- // If activity detected after warning, clear the warning
2342
- if (this.settings.enableDetailedLogging) {
2343
- console.log(
2344
- `[${id}] Connection activity detected after inactivity warning, resetting warning`
2345
- );
2346
- }
2347
- record.inactivityWarningIssued = false;
2348
- }
2349
- }
2350
- }
2351
-
2352
- // Log detailed stats periodically
2353
- console.log(
2354
- `Active connections: ${this.connectionRecords.size}. ` +
2355
- `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
2356
- `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}, ` +
2357
- `DomainSwitched=${domainSwitchedConnections}. ` +
2358
- `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
2359
- maxOutgoing
2360
- )}. ` +
2361
- `Termination stats: ${JSON.stringify({
2362
- IN: this.terminationStats.incoming,
2363
- OUT: this.terminationStats.outgoing,
2364
- })}`
2365
- );
2366
- }, this.settings.inactivityCheckInterval || 60000);
2367
-
2368
- // Make sure the interval doesn't keep the process alive
2369
- if (this.connectionLogger.unref) {
2370
- this.connectionLogger.unref();
2371
- }
2372
- }
2373
-
2374
- /**
2375
- * Gracefully shut down the proxy
2376
- */
2377
- public async stop() {
2378
- console.log('PortProxy shutting down...');
2379
- this.isShuttingDown = true;
2380
-
2381
- // Stop accepting new connections
2382
- const closeServerPromises: Promise<void>[] = this.netServers.map(
2383
- (server) =>
2384
- new Promise<void>((resolve) => {
2385
- if (!server.listening) {
2386
- resolve();
2387
- return;
2388
- }
2389
- server.close((err) => {
2390
- if (err) {
2391
- console.log(`Error closing server: ${err.message}`);
2392
- }
2393
- resolve();
2394
- });
2395
- })
2396
- );
2397
-
2398
- // Stop the connection logger
2399
- if (this.connectionLogger) {
2400
- clearInterval(this.connectionLogger);
2401
- this.connectionLogger = null;
2402
- }
2403
-
2404
- // Wait for servers to close
2405
- await Promise.all(closeServerPromises);
2406
- console.log('All servers closed. Cleaning up active connections...');
2407
-
2408
- // Force destroy all active connections immediately
2409
- const connectionIds = [...this.connectionRecords.keys()];
2410
- console.log(`Cleaning up ${connectionIds.length} active connections...`);
2411
-
2412
- // First pass: End all connections gracefully
2413
- for (const id of connectionIds) {
2414
- const record = this.connectionRecords.get(id);
2415
- if (record) {
2416
- try {
2417
- // Clear any timers
2418
- if (record.cleanupTimer) {
2419
- clearTimeout(record.cleanupTimer);
2420
- record.cleanupTimer = undefined;
2421
- }
2422
-
2423
- // End sockets gracefully
2424
- if (record.incoming && !record.incoming.destroyed) {
2425
- record.incoming.end();
2426
- }
2427
-
2428
- if (record.outgoing && !record.outgoing.destroyed) {
2429
- record.outgoing.end();
2430
- }
2431
- } catch (err) {
2432
- console.log(`Error during graceful connection end for ${id}: ${err}`);
2433
- }
2434
- }
2435
- }
2436
-
2437
- // Short delay to allow graceful ends to process
2438
- await new Promise((resolve) => setTimeout(resolve, 100));
2439
-
2440
- // Second pass: Force destroy everything
2441
- for (const id of connectionIds) {
2442
- const record = this.connectionRecords.get(id);
2443
- if (record) {
2444
- try {
2445
- // Remove all listeners to prevent memory leaks
2446
- if (record.incoming) {
2447
- record.incoming.removeAllListeners();
2448
- if (!record.incoming.destroyed) {
2449
- record.incoming.destroy();
2450
- }
2451
- }
2452
-
2453
- if (record.outgoing) {
2454
- record.outgoing.removeAllListeners();
2455
- if (!record.outgoing.destroyed) {
2456
- record.outgoing.destroy();
2457
- }
2458
- }
2459
- } catch (err) {
2460
- console.log(`Error during forced connection destruction for ${id}: ${err}`);
2461
- }
2462
- }
2463
- }
2464
-
2465
- // Stop NetworkProxy if it was started (which also stops ACME manager)
2466
- if (this.networkProxy) {
2467
- try {
2468
- console.log('Stopping NetworkProxy...');
2469
- await this.networkProxy.stop();
2470
- console.log('NetworkProxy stopped successfully');
2471
-
2472
- // Log ACME shutdown if it was enabled
2473
- if (this.settings.acme?.enabled) {
2474
- console.log('ACME certificate manager stopped');
2475
- }
2476
- } catch (err) {
2477
- console.log(`Error stopping NetworkProxy: ${err}`);
2478
- }
2479
- }
2480
-
2481
- // Clear all tracking maps
2482
- this.connectionRecords.clear();
2483
- this.domainTargetIndices.clear();
2484
- this.connectionsByIP.clear();
2485
- this.connectionRateByIP.clear();
2486
- this.netServers = [];
2487
-
2488
- // Reset termination stats
2489
- this.terminationStats = {
2490
- incoming: {},
2491
- outgoing: {},
2492
- };
2493
-
2494
- console.log('PortProxy shutdown complete.');
2495
- }
2496
- }