@push.rocks/smartproxy 3.41.8 → 4.1.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 (43) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/classes.pp.acmemanager.d.ts +34 -0
  3. package/dist_ts/classes.pp.acmemanager.js +123 -0
  4. package/dist_ts/classes.pp.connectionhandler.d.ts +39 -0
  5. package/dist_ts/classes.pp.connectionhandler.js +693 -0
  6. package/dist_ts/classes.pp.connectionmanager.d.ts +78 -0
  7. package/dist_ts/classes.pp.connectionmanager.js +378 -0
  8. package/dist_ts/classes.pp.domainconfigmanager.d.ts +55 -0
  9. package/dist_ts/classes.pp.domainconfigmanager.js +103 -0
  10. package/dist_ts/classes.pp.interfaces.d.ts +109 -0
  11. package/dist_ts/classes.pp.interfaces.js +2 -0
  12. package/dist_ts/classes.pp.networkproxybridge.d.ts +43 -0
  13. package/dist_ts/classes.pp.networkproxybridge.js +211 -0
  14. package/dist_ts/classes.pp.portproxy.d.ts +48 -0
  15. package/dist_ts/classes.pp.portproxy.js +268 -0
  16. package/dist_ts/classes.pp.portrangemanager.d.ts +56 -0
  17. package/dist_ts/classes.pp.portrangemanager.js +179 -0
  18. package/dist_ts/classes.pp.securitymanager.d.ts +47 -0
  19. package/dist_ts/classes.pp.securitymanager.js +126 -0
  20. package/dist_ts/classes.pp.snihandler.d.ts +198 -0
  21. package/dist_ts/classes.pp.snihandler.js +1210 -0
  22. package/dist_ts/classes.pp.timeoutmanager.d.ts +47 -0
  23. package/dist_ts/classes.pp.timeoutmanager.js +154 -0
  24. package/dist_ts/classes.pp.tlsmanager.d.ts +57 -0
  25. package/dist_ts/classes.pp.tlsmanager.js +132 -0
  26. package/dist_ts/index.d.ts +2 -2
  27. package/dist_ts/index.js +3 -3
  28. package/package.json +1 -1
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/classes.pp.acmemanager.ts +149 -0
  31. package/ts/classes.pp.connectionhandler.ts +982 -0
  32. package/ts/classes.pp.connectionmanager.ts +446 -0
  33. package/ts/classes.pp.domainconfigmanager.ts +123 -0
  34. package/ts/classes.pp.interfaces.ts +136 -0
  35. package/ts/classes.pp.networkproxybridge.ts +258 -0
  36. package/ts/classes.pp.portproxy.ts +344 -0
  37. package/ts/classes.pp.portrangemanager.ts +214 -0
  38. package/ts/classes.pp.securitymanager.ts +147 -0
  39. package/ts/{classes.snihandler.ts → classes.pp.snihandler.ts} +1 -1
  40. package/ts/classes.pp.timeoutmanager.ts +190 -0
  41. package/ts/classes.pp.tlsmanager.ts +206 -0
  42. package/ts/index.ts +2 -2
  43. package/ts/classes.portproxy.ts +0 -2503
@@ -1,2503 +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 || 120000, // 120 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
- // Set up piping immediately - don't delay this crucial step
858
- pipingEstablished = true;
859
-
860
- // Flush all pending data to target
861
- if (record.pendingData.length > 0) {
862
- const combinedData = Buffer.concat(record.pendingData);
863
-
864
- if (this.settings.enableDetailedLogging) {
865
- console.log(`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`);
866
- }
867
-
868
- // Write pending data immediately
869
- targetSocket.write(combinedData, (err) => {
870
- if (err) {
871
- console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
872
- return this.initiateCleanupOnce(record, 'write_error');
873
- }
874
- });
875
-
876
- // Clear the buffer now that we've processed it
877
- record.pendingData = [];
878
- record.pendingDataSize = 0;
879
- }
880
-
881
- // Setup piping in both directions without any delays
882
- socket.pipe(targetSocket);
883
- targetSocket.pipe(socket);
884
-
885
- // Resume the socket to ensure data flows - CRITICAL!
886
- socket.resume();
887
-
888
- // Process any data that might be queued in the interim
889
- if (dataQueue.length > 0) {
890
- // Write any remaining queued data directly to the target socket
891
- for (const chunk of dataQueue) {
892
- targetSocket.write(chunk);
893
- }
894
- // Clear the queue
895
- dataQueue.length = 0;
896
- queueSize = 0;
897
- }
898
-
899
- if (this.settings.enableDetailedLogging) {
900
- console.log(
901
- `[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
902
- `${
903
- serverName
904
- ? ` (SNI: ${serverName})`
905
- : domainConfig
906
- ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
907
- : ''
908
- }` +
909
- ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
910
- record.hasKeepAlive ? 'Yes' : 'No'
911
- }`
912
- );
913
- } else {
914
- console.log(
915
- `Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
916
- `${
917
- serverName
918
- ? ` (SNI: ${serverName})`
919
- : domainConfig
920
- ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
921
- : ''
922
- }`
923
- );
924
- }
925
-
926
-
927
- // Add the renegotiation handler for SNI validation with strict domain enforcement
928
- // This will be called after we've established piping
929
- if (serverName) {
930
- // Define a handler for checking renegotiation with improved detection
931
- const renegotiationHandler = (renegChunk: Buffer) => {
932
- // Only process if this looks like a TLS ClientHello
933
- if (SniHandler.isClientHello(renegChunk)) {
934
- try {
935
- // Extract SNI from ClientHello
936
- // Create a connection info object for the existing connection
937
- const connInfo = {
938
- sourceIp: record.remoteIP,
939
- sourcePort: record.incoming.remotePort || 0,
940
- destIp: record.incoming.localAddress || '',
941
- destPort: record.incoming.localPort || 0,
942
- };
943
-
944
- // Check for session tickets if allowSessionTicket is disabled
945
- if (this.settings.allowSessionTicket === false) {
946
- // Analyze for session resumption attempt (session ticket or PSK)
947
- const resumptionInfo = SniHandler.hasSessionResumption(
948
- renegChunk,
949
- this.settings.enableTlsDebugLogging
950
- );
951
-
952
- if (resumptionInfo.isResumption) {
953
- // Always log resumption attempt for easier debugging
954
- // Try to extract SNI for logging
955
- const extractedSNI = SniHandler.extractSNI(
956
- renegChunk,
957
- this.settings.enableTlsDebugLogging
958
- );
959
- console.log(
960
- `[${connectionId}] Session resumption detected in renegotiation. ` +
961
- `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
962
- `SNI value: ${extractedSNI || 'None'}, ` +
963
- `allowSessionTicket: ${this.settings.allowSessionTicket}`
964
- );
965
-
966
- // Block if there's session resumption without SNI
967
- if (!resumptionInfo.hasSNI) {
968
- console.log(
969
- `[${connectionId}] Session resumption detected in renegotiation without SNI and allowSessionTicket=false. ` +
970
- `Terminating connection to force new TLS handshake.`
971
- );
972
- this.initiateCleanupOnce(record, 'session_ticket_blocked');
973
- return;
974
- } else {
975
- if (this.settings.enableDetailedLogging) {
976
- console.log(
977
- `[${connectionId}] Session resumption with SNI detected in renegotiation. ` +
978
- `Allowing connection since SNI is present.`
979
- );
980
- }
981
- }
982
- }
983
- }
984
-
985
- const newSNI = SniHandler.extractSNIWithResumptionSupport(
986
- renegChunk,
987
- connInfo,
988
- this.settings.enableTlsDebugLogging
989
- );
990
-
991
- // Skip if no SNI was found
992
- if (!newSNI) return;
993
-
994
- // Handle SNI change during renegotiation - always terminate for domain switches
995
- if (newSNI !== record.lockedDomain) {
996
- // Log and terminate the connection for any SNI change
997
- console.log(
998
- `[${connectionId}] Renegotiation with different SNI: ${record.lockedDomain} -> ${newSNI}. ` +
999
- `Terminating connection - SNI domain switching is not allowed.`
1000
- );
1001
- this.initiateCleanupOnce(record, 'sni_mismatch');
1002
- } else if (this.settings.enableDetailedLogging) {
1003
- console.log(
1004
- `[${connectionId}] Renegotiation detected with same SNI: ${newSNI}. Allowing.`
1005
- );
1006
- }
1007
- } catch (err) {
1008
- console.log(
1009
- `[${connectionId}] Error processing ClientHello: ${err}. Allowing connection to continue.`
1010
- );
1011
- }
1012
- }
1013
- };
1014
-
1015
- // Store the handler in the connection record so we can remove it during cleanup
1016
- record.renegotiationHandler = renegotiationHandler;
1017
-
1018
- // The renegotiation handler is added when piping is established
1019
- // Making it part of setupPiping ensures proper sequencing of event handlers
1020
- socket.on('data', renegotiationHandler);
1021
-
1022
- if (this.settings.enableDetailedLogging) {
1023
- console.log(
1024
- `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
1025
- );
1026
- if (this.settings.allowSessionTicket === false) {
1027
- console.log(
1028
- `[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
1029
- );
1030
- }
1031
- }
1032
- }
1033
-
1034
- // Set connection timeout with simpler logic
1035
- if (record.cleanupTimer) {
1036
- clearTimeout(record.cleanupTimer);
1037
- }
1038
-
1039
- // For immortal keep-alive connections, skip setting a timeout completely
1040
- if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal') {
1041
- if (this.settings.enableDetailedLogging) {
1042
- console.log(
1043
- `[${connectionId}] Keep-alive connection with immortal treatment - no max lifetime`
1044
- );
1045
- }
1046
- // No cleanup timer for immortal connections
1047
- }
1048
- // For extended keep-alive connections, use extended timeout
1049
- else if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
1050
- const extendedTimeout = this.settings.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000; // 7 days
1051
- const safeTimeout = ensureSafeTimeout(extendedTimeout);
1052
-
1053
- record.cleanupTimer = setTimeout(() => {
1054
- console.log(
1055
- `[${connectionId}] Keep-alive connection from ${
1056
- record.remoteIP
1057
- } exceeded extended lifetime (${plugins.prettyMs(extendedTimeout)}), forcing cleanup.`
1058
- );
1059
- this.initiateCleanupOnce(record, 'extended_lifetime');
1060
- }, safeTimeout);
1061
-
1062
- // Make sure timeout doesn't keep the process alive
1063
- if (record.cleanupTimer.unref) {
1064
- record.cleanupTimer.unref();
1065
- }
1066
-
1067
- if (this.settings.enableDetailedLogging) {
1068
- console.log(
1069
- `[${connectionId}] Keep-alive connection with extended lifetime of ${plugins.prettyMs(
1070
- extendedTimeout
1071
- )}`
1072
- );
1073
- }
1074
- }
1075
- // For standard connections, use normal timeout
1076
- else {
1077
- // Use domain-specific timeout if available, otherwise use default
1078
- const connectionTimeout =
1079
- record.domainConfig?.connectionTimeout || this.settings.maxConnectionLifetime!;
1080
- const safeTimeout = ensureSafeTimeout(connectionTimeout);
1081
-
1082
- record.cleanupTimer = setTimeout(() => {
1083
- console.log(
1084
- `[${connectionId}] Connection from ${
1085
- record.remoteIP
1086
- } exceeded max lifetime (${plugins.prettyMs(connectionTimeout)}), forcing cleanup.`
1087
- );
1088
- this.initiateCleanupOnce(record, 'connection_timeout');
1089
- }, safeTimeout);
1090
-
1091
- // Make sure timeout doesn't keep the process alive
1092
- if (record.cleanupTimer.unref) {
1093
- record.cleanupTimer.unref();
1094
- }
1095
- }
1096
-
1097
- // Mark TLS handshake as complete for TLS connections
1098
- if (record.isTLS) {
1099
- record.tlsHandshakeComplete = true;
1100
-
1101
- if (this.settings.enableTlsDebugLogging) {
1102
- console.log(
1103
- `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
1104
- );
1105
- }
1106
- }
1107
- });
1108
- }
1109
-
1110
- /**
1111
- * Get connections count by IP
1112
- */
1113
- private getConnectionCountByIP(ip: string): number {
1114
- return this.connectionsByIP.get(ip)?.size || 0;
1115
- }
1116
-
1117
- /**
1118
- * Check and update connection rate for an IP
1119
- */
1120
- private checkConnectionRate(ip: string): boolean {
1121
- const now = Date.now();
1122
- const minute = 60 * 1000;
1123
-
1124
- if (!this.connectionRateByIP.has(ip)) {
1125
- this.connectionRateByIP.set(ip, [now]);
1126
- return true;
1127
- }
1128
-
1129
- // Get timestamps and filter out entries older than 1 minute
1130
- const timestamps = this.connectionRateByIP.get(ip)!.filter((time) => now - time < minute);
1131
- timestamps.push(now);
1132
- this.connectionRateByIP.set(ip, timestamps);
1133
-
1134
- // Check if rate exceeds limit
1135
- return timestamps.length <= this.settings.connectionRateLimitPerMinute!;
1136
- }
1137
-
1138
- /**
1139
- * Track connection by IP
1140
- */
1141
- private trackConnectionByIP(ip: string, connectionId: string): void {
1142
- if (!this.connectionsByIP.has(ip)) {
1143
- this.connectionsByIP.set(ip, new Set());
1144
- }
1145
- this.connectionsByIP.get(ip)!.add(connectionId);
1146
- }
1147
-
1148
- /**
1149
- * Remove connection tracking for an IP
1150
- */
1151
- private removeConnectionByIP(ip: string, connectionId: string): void {
1152
- if (this.connectionsByIP.has(ip)) {
1153
- const connections = this.connectionsByIP.get(ip)!;
1154
- connections.delete(connectionId);
1155
- if (connections.size === 0) {
1156
- this.connectionsByIP.delete(ip);
1157
- }
1158
- }
1159
- }
1160
-
1161
- /**
1162
- * Track connection termination statistic
1163
- */
1164
- private incrementTerminationStat(side: 'incoming' | 'outgoing', reason: string): void {
1165
- this.terminationStats[side][reason] = (this.terminationStats[side][reason] || 0) + 1;
1166
- }
1167
-
1168
- /**
1169
- * Cleans up a connection record.
1170
- * Destroys both incoming and outgoing sockets, clears timers, and removes the record.
1171
- * @param record - The connection record to clean up
1172
- * @param reason - Optional reason for cleanup (for logging)
1173
- */
1174
- private cleanupConnection(record: IConnectionRecord, reason: string = 'normal'): void {
1175
- if (!record.connectionClosed) {
1176
- record.connectionClosed = true;
1177
-
1178
- // Track connection termination
1179
- this.removeConnectionByIP(record.remoteIP, record.id);
1180
-
1181
- if (record.cleanupTimer) {
1182
- clearTimeout(record.cleanupTimer);
1183
- record.cleanupTimer = undefined;
1184
- }
1185
-
1186
- // Detailed logging data
1187
- const duration = Date.now() - record.incomingStartTime;
1188
- const bytesReceived = record.bytesReceived;
1189
- const bytesSent = record.bytesSent;
1190
-
1191
- // Remove all data handlers (both standard and renegotiation) to make sure we clean up properly
1192
- if (record.incoming) {
1193
- try {
1194
- // Remove our safe data handler
1195
- record.incoming.removeAllListeners('data');
1196
-
1197
- // Reset the handler references
1198
- record.renegotiationHandler = undefined;
1199
- } catch (err) {
1200
- console.log(`[${record.id}] Error removing data handlers: ${err}`);
1201
- }
1202
- }
1203
-
1204
- try {
1205
- if (!record.incoming.destroyed) {
1206
- // Try graceful shutdown first, then force destroy after a short timeout
1207
- record.incoming.end();
1208
- const incomingTimeout = setTimeout(() => {
1209
- try {
1210
- if (record && !record.incoming.destroyed) {
1211
- record.incoming.destroy();
1212
- }
1213
- } catch (err) {
1214
- console.log(`[${record.id}] Error destroying incoming socket: ${err}`);
1215
- }
1216
- }, 1000);
1217
-
1218
- // Ensure the timeout doesn't block Node from exiting
1219
- if (incomingTimeout.unref) {
1220
- incomingTimeout.unref();
1221
- }
1222
- }
1223
- } catch (err) {
1224
- console.log(`[${record.id}] Error closing incoming socket: ${err}`);
1225
- try {
1226
- if (!record.incoming.destroyed) {
1227
- record.incoming.destroy();
1228
- }
1229
- } catch (destroyErr) {
1230
- console.log(`[${record.id}] Error destroying incoming socket: ${destroyErr}`);
1231
- }
1232
- }
1233
-
1234
- try {
1235
- if (record.outgoing && !record.outgoing.destroyed) {
1236
- // Try graceful shutdown first, then force destroy after a short timeout
1237
- record.outgoing.end();
1238
- const outgoingTimeout = setTimeout(() => {
1239
- try {
1240
- if (record && record.outgoing && !record.outgoing.destroyed) {
1241
- record.outgoing.destroy();
1242
- }
1243
- } catch (err) {
1244
- console.log(`[${record.id}] Error destroying outgoing socket: ${err}`);
1245
- }
1246
- }, 1000);
1247
-
1248
- // Ensure the timeout doesn't block Node from exiting
1249
- if (outgoingTimeout.unref) {
1250
- outgoingTimeout.unref();
1251
- }
1252
- }
1253
- } catch (err) {
1254
- console.log(`[${record.id}] Error closing outgoing socket: ${err}`);
1255
- try {
1256
- if (record.outgoing && !record.outgoing.destroyed) {
1257
- record.outgoing.destroy();
1258
- }
1259
- } catch (destroyErr) {
1260
- console.log(`[${record.id}] Error destroying outgoing socket: ${destroyErr}`);
1261
- }
1262
- }
1263
-
1264
- // Clear pendingData to avoid memory leaks
1265
- record.pendingData = [];
1266
- record.pendingDataSize = 0;
1267
-
1268
- // Remove the record from the tracking map
1269
- this.connectionRecords.delete(record.id);
1270
-
1271
- // Log connection details
1272
- if (this.settings.enableDetailedLogging) {
1273
- console.log(
1274
- `[${record.id}] Connection from ${record.remoteIP} on port ${record.localPort} terminated (${reason}).` +
1275
- ` Duration: ${plugins.prettyMs(
1276
- duration
1277
- )}, Bytes IN: ${bytesReceived}, OUT: ${bytesSent}, ` +
1278
- `TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1279
- record.hasKeepAlive ? 'Yes' : 'No'
1280
- }` +
1281
- `${record.usingNetworkProxy ? ', Using NetworkProxy' : ''}` +
1282
- `${record.domainSwitches ? `, Domain switches: ${record.domainSwitches}` : ''}`
1283
- );
1284
- } else {
1285
- console.log(
1286
- `[${record.id}] Connection from ${record.remoteIP} terminated (${reason}). Active connections: ${this.connectionRecords.size}`
1287
- );
1288
- }
1289
- }
1290
- }
1291
-
1292
- /**
1293
- * Update connection activity timestamp
1294
- */
1295
- private updateActivity(record: IConnectionRecord): void {
1296
- record.lastActivity = Date.now();
1297
-
1298
- // Clear any inactivity warning
1299
- if (record.inactivityWarningIssued) {
1300
- record.inactivityWarningIssued = false;
1301
- }
1302
- }
1303
-
1304
- /**
1305
- * Get target IP with round-robin support
1306
- */
1307
- private getTargetIP(domainConfig: IDomainConfig): string {
1308
- if (domainConfig.targetIPs && domainConfig.targetIPs.length > 0) {
1309
- const currentIndex = this.domainTargetIndices.get(domainConfig) || 0;
1310
- const ip = domainConfig.targetIPs[currentIndex % domainConfig.targetIPs.length];
1311
- this.domainTargetIndices.set(domainConfig, currentIndex + 1);
1312
- return ip;
1313
- }
1314
- return this.settings.targetIP!;
1315
- }
1316
-
1317
- /**
1318
- * Initiates cleanup once for a connection
1319
- */
1320
- private initiateCleanupOnce(record: IConnectionRecord, reason: string = 'normal'): void {
1321
- if (this.settings.enableDetailedLogging) {
1322
- console.log(`[${record.id}] Connection cleanup initiated for ${record.remoteIP} (${reason})`);
1323
- }
1324
-
1325
- if (
1326
- record.incomingTerminationReason === null ||
1327
- record.incomingTerminationReason === undefined
1328
- ) {
1329
- record.incomingTerminationReason = reason;
1330
- this.incrementTerminationStat('incoming', reason);
1331
- }
1332
-
1333
- this.cleanupConnection(record, reason);
1334
- }
1335
-
1336
- /**
1337
- * Creates a generic error handler for incoming or outgoing sockets
1338
- */
1339
- private handleError(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
1340
- return (err: Error) => {
1341
- const code = (err as any).code;
1342
- let reason = 'error';
1343
-
1344
- const now = Date.now();
1345
- const connectionDuration = now - record.incomingStartTime;
1346
- const lastActivityAge = now - record.lastActivity;
1347
-
1348
- if (code === 'ECONNRESET') {
1349
- reason = 'econnreset';
1350
- console.log(
1351
- `[${record.id}] ECONNRESET on ${side} side from ${record.remoteIP}: ${
1352
- err.message
1353
- }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
1354
- lastActivityAge
1355
- )} ago`
1356
- );
1357
- } else if (code === 'ETIMEDOUT') {
1358
- reason = 'etimedout';
1359
- console.log(
1360
- `[${record.id}] ETIMEDOUT on ${side} side from ${record.remoteIP}: ${
1361
- err.message
1362
- }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
1363
- lastActivityAge
1364
- )} ago`
1365
- );
1366
- } else {
1367
- console.log(
1368
- `[${record.id}] Error on ${side} side from ${record.remoteIP}: ${
1369
- err.message
1370
- }. Duration: ${plugins.prettyMs(connectionDuration)}, Last activity: ${plugins.prettyMs(
1371
- lastActivityAge
1372
- )} ago`
1373
- );
1374
- }
1375
-
1376
- if (side === 'incoming' && record.incomingTerminationReason === null) {
1377
- record.incomingTerminationReason = reason;
1378
- this.incrementTerminationStat('incoming', reason);
1379
- } else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
1380
- record.outgoingTerminationReason = reason;
1381
- this.incrementTerminationStat('outgoing', reason);
1382
- }
1383
-
1384
- this.initiateCleanupOnce(record, reason);
1385
- };
1386
- }
1387
-
1388
- /**
1389
- * Creates a generic close handler for incoming or outgoing sockets
1390
- */
1391
- private handleClose(side: 'incoming' | 'outgoing', record: IConnectionRecord) {
1392
- return () => {
1393
- if (this.settings.enableDetailedLogging) {
1394
- console.log(`[${record.id}] Connection closed on ${side} side from ${record.remoteIP}`);
1395
- }
1396
-
1397
- if (side === 'incoming' && record.incomingTerminationReason === null) {
1398
- record.incomingTerminationReason = 'normal';
1399
- this.incrementTerminationStat('incoming', 'normal');
1400
- } else if (side === 'outgoing' && record.outgoingTerminationReason === null) {
1401
- record.outgoingTerminationReason = 'normal';
1402
- this.incrementTerminationStat('outgoing', 'normal');
1403
- // Record the time when outgoing socket closed.
1404
- record.outgoingClosedTime = Date.now();
1405
- }
1406
-
1407
- this.initiateCleanupOnce(record, 'closed_' + side);
1408
- };
1409
- }
1410
-
1411
- /**
1412
- * Main method to start the proxy
1413
- */
1414
- public async start() {
1415
- // Don't start if already shutting down
1416
- if (this.isShuttingDown) {
1417
- console.log("Cannot start PortProxy while it's shutting down");
1418
- return;
1419
- }
1420
-
1421
- // Initialize NetworkProxy if needed (useNetworkProxy is set but networkProxy isn't initialized)
1422
- if (
1423
- this.settings.useNetworkProxy &&
1424
- this.settings.useNetworkProxy.length > 0 &&
1425
- !this.networkProxy
1426
- ) {
1427
- await this.initializeNetworkProxy();
1428
- }
1429
-
1430
- // Start NetworkProxy if configured
1431
- if (this.networkProxy) {
1432
- await this.networkProxy.start();
1433
- console.log(`NetworkProxy started on port ${this.settings.networkProxyPort}`);
1434
-
1435
- // Log ACME status
1436
- if (this.settings.acme?.enabled) {
1437
- console.log(
1438
- `ACME certificate management is enabled (${
1439
- this.settings.acme.useProduction ? 'Production' : 'Staging'
1440
- } mode)`
1441
- );
1442
- console.log(`ACME HTTP challenge server on port ${this.settings.acme.port}`);
1443
-
1444
- // Register domains for ACME certificates if enabled
1445
- if (this.networkProxy.options.acme?.enabled) {
1446
- console.log('Registering domains with ACME certificate manager...');
1447
- // The NetworkProxy will handle this internally via registerDomainsWithAcmeManager()
1448
- }
1449
- }
1450
- }
1451
-
1452
- // Define a unified connection handler for all listening ports.
1453
- const connectionHandler = (socket: plugins.net.Socket) => {
1454
- if (this.isShuttingDown) {
1455
- socket.end();
1456
- socket.destroy();
1457
- return;
1458
- }
1459
-
1460
- const remoteIP = socket.remoteAddress || '';
1461
- const localPort = socket.localPort || 0; // The port on which this connection was accepted.
1462
-
1463
- // Check rate limits
1464
- if (
1465
- this.settings.maxConnectionsPerIP &&
1466
- this.getConnectionCountByIP(remoteIP) >= this.settings.maxConnectionsPerIP
1467
- ) {
1468
- console.log(
1469
- `Connection rejected from ${remoteIP}: Maximum connections per IP (${this.settings.maxConnectionsPerIP}) exceeded`
1470
- );
1471
- socket.end();
1472
- socket.destroy();
1473
- return;
1474
- }
1475
-
1476
- if (this.settings.connectionRateLimitPerMinute && !this.checkConnectionRate(remoteIP)) {
1477
- console.log(
1478
- `Connection rejected from ${remoteIP}: Connection rate limit (${this.settings.connectionRateLimitPerMinute}/min) exceeded`
1479
- );
1480
- socket.end();
1481
- socket.destroy();
1482
- return;
1483
- }
1484
-
1485
- // Apply socket optimizations
1486
- socket.setNoDelay(this.settings.noDelay);
1487
-
1488
- // Create a unique connection ID and record
1489
- const connectionId = generateConnectionId();
1490
- const connectionRecord: IConnectionRecord = {
1491
- id: connectionId,
1492
- incoming: socket,
1493
- outgoing: null,
1494
- incomingStartTime: Date.now(),
1495
- lastActivity: Date.now(),
1496
- connectionClosed: false,
1497
- pendingData: [],
1498
- pendingDataSize: 0,
1499
-
1500
- // Initialize enhanced tracking fields
1501
- bytesReceived: 0,
1502
- bytesSent: 0,
1503
- remoteIP: remoteIP,
1504
- localPort: localPort,
1505
- isTLS: false,
1506
- tlsHandshakeComplete: false,
1507
- hasReceivedInitialData: false,
1508
- hasKeepAlive: false, // Will set to true if keep-alive is applied
1509
- incomingTerminationReason: null,
1510
- outgoingTerminationReason: null,
1511
-
1512
- // Initialize NetworkProxy tracking
1513
- usingNetworkProxy: false,
1514
-
1515
- // Initialize browser connection tracking
1516
- isBrowserConnection: false,
1517
- domainSwitches: 0,
1518
- };
1519
-
1520
- // Apply keep-alive settings if enabled
1521
- if (this.settings.keepAlive) {
1522
- socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
1523
- connectionRecord.hasKeepAlive = true; // Mark connection as having keep-alive
1524
-
1525
- // Apply enhanced TCP keep-alive options if enabled
1526
- if (this.settings.enableKeepAliveProbes) {
1527
- try {
1528
- // These are platform-specific and may not be available
1529
- if ('setKeepAliveProbes' in socket) {
1530
- (socket as any).setKeepAliveProbes(10); // More aggressive probing
1531
- }
1532
- if ('setKeepAliveInterval' in socket) {
1533
- (socket as any).setKeepAliveInterval(1000); // 1 second interval between probes
1534
- }
1535
- } catch (err) {
1536
- // Ignore errors - these are optional enhancements
1537
- if (this.settings.enableDetailedLogging) {
1538
- console.log(
1539
- `[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`
1540
- );
1541
- }
1542
- }
1543
- }
1544
- }
1545
-
1546
- // Track connection by IP
1547
- this.trackConnectionByIP(remoteIP, connectionId);
1548
- this.connectionRecords.set(connectionId, connectionRecord);
1549
-
1550
- if (this.settings.enableDetailedLogging) {
1551
- console.log(
1552
- `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
1553
- `Keep-Alive: ${connectionRecord.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
1554
- `Active connections: ${this.connectionRecords.size}`
1555
- );
1556
- } else {
1557
- console.log(
1558
- `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionRecords.size}`
1559
- );
1560
- }
1561
-
1562
- // Check if this connection should be forwarded directly to NetworkProxy
1563
- // First check port-based forwarding settings
1564
- let shouldUseNetworkProxy =
1565
- this.settings.useNetworkProxy && this.settings.useNetworkProxy.includes(localPort);
1566
-
1567
- // We'll look for domain-specific settings after SNI extraction
1568
-
1569
- if (shouldUseNetworkProxy) {
1570
- // For NetworkProxy ports, we want to capture the TLS handshake and forward directly
1571
- let initialDataReceived = false;
1572
-
1573
- // Set an initial timeout for handshake data
1574
- let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
1575
- if (!initialDataReceived) {
1576
- console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP}`);
1577
-
1578
- // Add a grace period instead of immediate termination
1579
- setTimeout(() => {
1580
- if (!initialDataReceived) {
1581
- console.log(`[${connectionId}] Final initial data timeout after grace period`);
1582
- if (connectionRecord.incomingTerminationReason === null) {
1583
- connectionRecord.incomingTerminationReason = 'initial_timeout';
1584
- this.incrementTerminationStat('incoming', 'initial_timeout');
1585
- }
1586
- socket.end();
1587
- this.cleanupConnection(connectionRecord, 'initial_timeout');
1588
- }
1589
- }, 30000); // 30 second grace period
1590
- }
1591
- }, this.settings.initialDataTimeout!);
1592
-
1593
- // Make sure timeout doesn't keep the process alive
1594
- if (initialTimeout.unref) {
1595
- initialTimeout.unref();
1596
- }
1597
-
1598
- socket.on('error', this.handleError('incoming', connectionRecord));
1599
-
1600
- // First data handler to capture initial TLS handshake for NetworkProxy
1601
- socket.once('data', (chunk: Buffer) => {
1602
- // Clear the initial timeout since we've received data
1603
- if (initialTimeout) {
1604
- clearTimeout(initialTimeout);
1605
- initialTimeout = null;
1606
- }
1607
-
1608
- initialDataReceived = true;
1609
- connectionRecord.hasReceivedInitialData = true;
1610
-
1611
- // Block non-TLS connections on port 443
1612
- // Always enforce TLS on standard HTTPS port
1613
- if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) {
1614
- console.log(
1615
- `[${connectionId}] Non-TLS connection detected on port 443. ` +
1616
- `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
1617
- );
1618
- if (connectionRecord.incomingTerminationReason === null) {
1619
- connectionRecord.incomingTerminationReason = 'non_tls_blocked';
1620
- this.incrementTerminationStat('incoming', 'non_tls_blocked');
1621
- }
1622
- socket.end();
1623
- this.cleanupConnection(connectionRecord, 'non_tls_blocked');
1624
- return;
1625
- }
1626
-
1627
- // Check if this looks like a TLS handshake
1628
- if (SniHandler.isTlsHandshake(chunk)) {
1629
- connectionRecord.isTLS = true;
1630
-
1631
- // Check for TLS ClientHello with either no SNI or session tickets
1632
- if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
1633
- // Extract SNI first
1634
- const extractedSNI = SniHandler.extractSNI(
1635
- chunk,
1636
- this.settings.enableTlsDebugLogging
1637
- );
1638
- const hasSNI = !!extractedSNI;
1639
-
1640
- // Analyze for session resumption attempt
1641
- const resumptionInfo = SniHandler.hasSessionResumption(
1642
- chunk,
1643
- this.settings.enableTlsDebugLogging
1644
- );
1645
-
1646
- // Always log for debugging purposes
1647
- console.log(
1648
- `[${connectionId}] TLS ClientHello detected with allowSessionTicket=false. ` +
1649
- `Has SNI: ${hasSNI ? 'Yes' : 'No'}, ` +
1650
- `SNI value: ${extractedSNI || 'None'}, ` +
1651
- `Has session resumption: ${resumptionInfo.isResumption ? 'Yes' : 'No'}`
1652
- );
1653
-
1654
- // Block if this is a connection with session resumption but no SNI
1655
- if (resumptionInfo.isResumption && !hasSNI) {
1656
- console.log(
1657
- `[${connectionId}] Session resumption detected in initial ClientHello without SNI and allowSessionTicket=false. ` +
1658
- `Terminating connection to force new TLS handshake.`
1659
- );
1660
- if (connectionRecord.incomingTerminationReason === null) {
1661
- connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
1662
- this.incrementTerminationStat('incoming', 'session_ticket_blocked');
1663
- }
1664
- socket.end();
1665
- this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
1666
- return;
1667
- }
1668
-
1669
- // Also block if this is a TLS connection without SNI when allowSessionTicket is false
1670
- // This forces clients to send SNI which helps with routing
1671
- if (!hasSNI && localPort === 443) {
1672
- console.log(
1673
- `[${connectionId}] TLS ClientHello detected on port 443 without SNI and allowSessionTicket=false. ` +
1674
- `Terminating connection to force proper SNI in handshake.`
1675
- );
1676
- if (connectionRecord.incomingTerminationReason === null) {
1677
- connectionRecord.incomingTerminationReason = 'no_sni_blocked';
1678
- this.incrementTerminationStat('incoming', 'no_sni_blocked');
1679
- }
1680
- socket.end();
1681
- this.cleanupConnection(connectionRecord, 'no_sni_blocked');
1682
- return;
1683
- }
1684
- }
1685
-
1686
- // Try to extract SNI for domain-specific NetworkProxy handling
1687
- const connInfo = {
1688
- sourceIp: remoteIP,
1689
- sourcePort: socket.remotePort || 0,
1690
- destIp: socket.localAddress || '',
1691
- destPort: socket.localPort || 0,
1692
- };
1693
-
1694
- // Extract SNI to check for domain-specific NetworkProxy settings
1695
- const serverName = SniHandler.processTlsPacket(
1696
- chunk,
1697
- connInfo,
1698
- this.settings.enableTlsDebugLogging
1699
- );
1700
-
1701
- if (serverName) {
1702
- // If we got an SNI, check for domain-specific NetworkProxy settings
1703
- const domainConfig = this.settings.domainConfigs.find((config) =>
1704
- config.domains.some((d) => plugins.minimatch(serverName, d))
1705
- );
1706
-
1707
- // Save domain config and SNI in connection record
1708
- connectionRecord.domainConfig = domainConfig;
1709
- connectionRecord.lockedDomain = serverName;
1710
-
1711
- // Use domain-specific NetworkProxy port if configured
1712
- if (domainConfig?.useNetworkProxy) {
1713
- const networkProxyPort =
1714
- domainConfig.networkProxyPort || this.settings.networkProxyPort;
1715
-
1716
- if (this.settings.enableDetailedLogging) {
1717
- console.log(
1718
- `[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
1719
- );
1720
- }
1721
-
1722
- // Forward to NetworkProxy with domain-specific port
1723
- this.forwardToNetworkProxy(
1724
- connectionId,
1725
- socket,
1726
- connectionRecord,
1727
- chunk,
1728
- networkProxyPort
1729
- );
1730
- return;
1731
- }
1732
- }
1733
-
1734
- // Forward directly to NetworkProxy without domain-specific settings
1735
- this.forwardToNetworkProxy(connectionId, socket, connectionRecord, chunk);
1736
- } else {
1737
- // If not TLS, use normal direct connection
1738
- console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${localPort}`);
1739
- this.setupDirectConnection(
1740
- connectionId,
1741
- socket,
1742
- connectionRecord,
1743
- undefined,
1744
- undefined,
1745
- chunk
1746
- );
1747
- }
1748
- });
1749
- } else {
1750
- // For non-NetworkProxy ports, proceed with normal processing
1751
-
1752
- // Define helpers for rejecting connections
1753
- const rejectIncomingConnection = (reason: string, logMessage: string) => {
1754
- console.log(`[${connectionId}] ${logMessage}`);
1755
- socket.end();
1756
- if (connectionRecord.incomingTerminationReason === null) {
1757
- connectionRecord.incomingTerminationReason = reason;
1758
- this.incrementTerminationStat('incoming', reason);
1759
- }
1760
- this.cleanupConnection(connectionRecord, reason);
1761
- };
1762
-
1763
- let initialDataReceived = false;
1764
-
1765
- // Set an initial timeout for SNI data if needed
1766
- let initialTimeout: NodeJS.Timeout | null = null;
1767
- if (this.settings.sniEnabled) {
1768
- initialTimeout = setTimeout(() => {
1769
- if (!initialDataReceived) {
1770
- console.log(`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${remoteIP}`);
1771
-
1772
- // Add a grace period instead of immediate termination
1773
- setTimeout(() => {
1774
- if (!initialDataReceived) {
1775
- console.log(`[${connectionId}] Final initial data timeout after grace period`);
1776
- if (connectionRecord.incomingTerminationReason === null) {
1777
- connectionRecord.incomingTerminationReason = 'initial_timeout';
1778
- this.incrementTerminationStat('incoming', 'initial_timeout');
1779
- }
1780
- socket.end();
1781
- this.cleanupConnection(connectionRecord, 'initial_timeout');
1782
- }
1783
- }, 30000); // 30 second grace period
1784
- }
1785
- }, this.settings.initialDataTimeout!);
1786
-
1787
- // Make sure timeout doesn't keep the process alive
1788
- if (initialTimeout.unref) {
1789
- initialTimeout.unref();
1790
- }
1791
- } else {
1792
- initialDataReceived = true;
1793
- connectionRecord.hasReceivedInitialData = true;
1794
- }
1795
-
1796
- socket.on('error', this.handleError('incoming', connectionRecord));
1797
-
1798
- // Track data for bytes counting
1799
- socket.on('data', (chunk: Buffer) => {
1800
- connectionRecord.bytesReceived += chunk.length;
1801
- this.updateActivity(connectionRecord);
1802
-
1803
- // Check for TLS handshake if this is the first chunk
1804
- if (!connectionRecord.isTLS && SniHandler.isTlsHandshake(chunk)) {
1805
- connectionRecord.isTLS = true;
1806
-
1807
- if (this.settings.enableTlsDebugLogging) {
1808
- console.log(
1809
- `[${connectionId}] TLS handshake detected from ${remoteIP}, ${chunk.length} bytes`
1810
- );
1811
- // Try to extract SNI and log detailed debug info
1812
- // Create connection info for debug logging
1813
- const debugConnInfo = {
1814
- sourceIp: remoteIP,
1815
- sourcePort: socket.remotePort || 0,
1816
- destIp: socket.localAddress || '',
1817
- destPort: socket.localPort || 0,
1818
- };
1819
-
1820
- SniHandler.extractSNIWithResumptionSupport(chunk, debugConnInfo, true);
1821
- }
1822
- }
1823
- });
1824
-
1825
- /**
1826
- * Sets up the connection to the target host.
1827
- * @param serverName - The SNI hostname (unused when forcedDomain is provided).
1828
- * @param initialChunk - Optional initial data chunk.
1829
- * @param forcedDomain - If provided, overrides SNI/domain lookup (used for port-based routing).
1830
- * @param overridePort - If provided, use this port for the outgoing connection.
1831
- */
1832
- const setupConnection = (
1833
- serverName: string,
1834
- initialChunk?: Buffer,
1835
- forcedDomain?: IDomainConfig,
1836
- overridePort?: number
1837
- ) => {
1838
- // Clear the initial timeout since we've received data
1839
- if (initialTimeout) {
1840
- clearTimeout(initialTimeout);
1841
- initialTimeout = null;
1842
- }
1843
-
1844
- // Mark that we've received initial data
1845
- initialDataReceived = true;
1846
- connectionRecord.hasReceivedInitialData = true;
1847
-
1848
- // Check if this looks like a TLS handshake
1849
- const isTlsHandshakeDetected = initialChunk && SniHandler.isTlsHandshake(initialChunk);
1850
- if (isTlsHandshakeDetected) {
1851
- connectionRecord.isTLS = true;
1852
-
1853
- if (this.settings.enableTlsDebugLogging) {
1854
- console.log(
1855
- `[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
1856
- );
1857
- }
1858
- }
1859
-
1860
- // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
1861
- const domainConfig = forcedDomain
1862
- ? forcedDomain
1863
- : serverName
1864
- ? this.settings.domainConfigs.find((config) =>
1865
- config.domains.some((d) => plugins.minimatch(serverName, d))
1866
- )
1867
- : undefined;
1868
-
1869
- // Save domain config in connection record
1870
- connectionRecord.domainConfig = domainConfig;
1871
-
1872
- // Check if this domain should use NetworkProxy (domain-specific setting)
1873
- if (domainConfig?.useNetworkProxy && this.networkProxy) {
1874
- if (this.settings.enableDetailedLogging) {
1875
- console.log(
1876
- `[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`
1877
- );
1878
- }
1879
-
1880
- const networkProxyPort =
1881
- domainConfig.networkProxyPort || this.settings.networkProxyPort;
1882
-
1883
- if (initialChunk && connectionRecord.isTLS) {
1884
- // For TLS connections with initial chunk, forward to NetworkProxy
1885
- this.forwardToNetworkProxy(
1886
- connectionId,
1887
- socket,
1888
- connectionRecord,
1889
- initialChunk,
1890
- networkProxyPort // Pass the domain-specific NetworkProxy port if configured
1891
- );
1892
- return; // Skip normal connection setup
1893
- }
1894
- }
1895
-
1896
- // IP validation is skipped if allowedIPs is empty
1897
- if (domainConfig) {
1898
- const effectiveAllowedIPs: string[] = [
1899
- ...domainConfig.allowedIPs,
1900
- ...(this.settings.defaultAllowedIPs || []),
1901
- ];
1902
- const effectiveBlockedIPs: string[] = [
1903
- ...(domainConfig.blockedIPs || []),
1904
- ...(this.settings.defaultBlockedIPs || []),
1905
- ];
1906
-
1907
- // Skip IP validation if allowedIPs is empty
1908
- if (
1909
- domainConfig.allowedIPs.length > 0 &&
1910
- !isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)
1911
- ) {
1912
- return rejectIncomingConnection(
1913
- 'rejected',
1914
- `Connection rejected: IP ${remoteIP} not allowed for domain ${domainConfig.domains.join(
1915
- ', '
1916
- )}`
1917
- );
1918
- }
1919
- } else if (
1920
- this.settings.defaultAllowedIPs &&
1921
- this.settings.defaultAllowedIPs.length > 0
1922
- ) {
1923
- if (
1924
- !isGlobIPAllowed(
1925
- remoteIP,
1926
- this.settings.defaultAllowedIPs,
1927
- this.settings.defaultBlockedIPs || []
1928
- )
1929
- ) {
1930
- return rejectIncomingConnection(
1931
- 'rejected',
1932
- `Connection rejected: IP ${remoteIP} not allowed by default allowed list`
1933
- );
1934
- }
1935
- }
1936
-
1937
- // Save the initial SNI
1938
- if (serverName) {
1939
- connectionRecord.lockedDomain = serverName;
1940
- }
1941
-
1942
- // Set up the direct connection
1943
- return this.setupDirectConnection(
1944
- connectionId,
1945
- socket,
1946
- connectionRecord,
1947
- domainConfig,
1948
- serverName,
1949
- initialChunk,
1950
- overridePort
1951
- );
1952
- };
1953
-
1954
- // --- PORT RANGE-BASED HANDLING ---
1955
- // Only apply port-based rules if the incoming port is within one of the global port ranges.
1956
- if (
1957
- this.settings.globalPortRanges &&
1958
- isPortInRanges(localPort, this.settings.globalPortRanges)
1959
- ) {
1960
- if (this.settings.forwardAllGlobalRanges) {
1961
- if (
1962
- this.settings.defaultAllowedIPs &&
1963
- this.settings.defaultAllowedIPs.length > 0 &&
1964
- !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
1965
- ) {
1966
- console.log(
1967
- `[${connectionId}] Connection from ${remoteIP} rejected: IP ${remoteIP} not allowed in global default allowed list.`
1968
- );
1969
- socket.end();
1970
- return;
1971
- }
1972
- if (this.settings.enableDetailedLogging) {
1973
- console.log(
1974
- `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
1975
- );
1976
- }
1977
- setupConnection(
1978
- '',
1979
- undefined,
1980
- {
1981
- domains: ['global'],
1982
- allowedIPs: this.settings.defaultAllowedIPs || [],
1983
- blockedIPs: this.settings.defaultBlockedIPs || [],
1984
- targetIPs: [this.settings.targetIP!],
1985
- portRanges: [],
1986
- },
1987
- localPort
1988
- );
1989
- return;
1990
- } else {
1991
- // Attempt to find a matching forced domain config based on the local port.
1992
- const forcedDomain = this.settings.domainConfigs.find(
1993
- (domain) =>
1994
- domain.portRanges &&
1995
- domain.portRanges.length > 0 &&
1996
- isPortInRanges(localPort, domain.portRanges)
1997
- );
1998
- if (forcedDomain) {
1999
- const effectiveAllowedIPs: string[] = [
2000
- ...forcedDomain.allowedIPs,
2001
- ...(this.settings.defaultAllowedIPs || []),
2002
- ];
2003
- const effectiveBlockedIPs: string[] = [
2004
- ...(forcedDomain.blockedIPs || []),
2005
- ...(this.settings.defaultBlockedIPs || []),
2006
- ];
2007
- if (!isGlobIPAllowed(remoteIP, effectiveAllowedIPs, effectiveBlockedIPs)) {
2008
- console.log(
2009
- `[${connectionId}] Connection from ${remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
2010
- ', '
2011
- )} on port ${localPort}.`
2012
- );
2013
- socket.end();
2014
- return;
2015
- }
2016
- if (this.settings.enableDetailedLogging) {
2017
- console.log(
2018
- `[${connectionId}] Port-based connection from ${remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
2019
- ', '
2020
- )}.`
2021
- );
2022
- }
2023
- setupConnection('', undefined, forcedDomain, localPort);
2024
- return;
2025
- }
2026
- // Fall through to SNI/default handling if no forced domain config is found.
2027
- }
2028
- }
2029
-
2030
- // --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
2031
- if (this.settings.sniEnabled) {
2032
- initialDataReceived = false;
2033
-
2034
- socket.once('data', (chunk: Buffer) => {
2035
- // Clear timeout immediately
2036
- if (initialTimeout) {
2037
- clearTimeout(initialTimeout);
2038
- initialTimeout = null;
2039
- }
2040
-
2041
- initialDataReceived = true;
2042
-
2043
- // Add debugging ONLY if detailed logging is enabled - avoid heavy processing
2044
- if (this.settings.enableTlsDebugLogging && SniHandler.isClientHello(chunk)) {
2045
- // Move heavy debug logging to a separate async task to not block the flow
2046
- setImmediate(() => {
2047
- try {
2048
- const resumptionInfo = SniHandler.hasSessionResumption(chunk, true);
2049
- const standardSNI = SniHandler.extractSNI(chunk, true);
2050
- const pskSNI = SniHandler.extractSNIFromPSKExtension(chunk, true);
2051
-
2052
- console.log(`[${connectionId}] ClientHello details: isResumption=${resumptionInfo.isResumption}, hasSNI=${resumptionInfo.hasSNI}`);
2053
- console.log(`[${connectionId}] SNI extraction results: standardSNI=${standardSNI || 'none'}, pskSNI=${pskSNI || 'none'}`);
2054
- } catch (err) {
2055
- console.log(`[${connectionId}] Error in debug logging: ${err}`);
2056
- }
2057
- });
2058
- }
2059
-
2060
- // Block non-TLS connections on port 443
2061
- // Always enforce TLS on standard HTTPS port
2062
- if (!SniHandler.isTlsHandshake(chunk) && localPort === 443) {
2063
- console.log(
2064
- `[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
2065
- `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
2066
- );
2067
- if (connectionRecord.incomingTerminationReason === null) {
2068
- connectionRecord.incomingTerminationReason = 'non_tls_blocked';
2069
- this.incrementTerminationStat('incoming', 'non_tls_blocked');
2070
- }
2071
- socket.end();
2072
- this.cleanupConnection(connectionRecord, 'non_tls_blocked');
2073
- return;
2074
- }
2075
-
2076
- // Try to extract SNI
2077
- let serverName = '';
2078
-
2079
- if (SniHandler.isTlsHandshake(chunk)) {
2080
- connectionRecord.isTLS = true;
2081
-
2082
- if (this.settings.enableTlsDebugLogging) {
2083
- console.log(
2084
- `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
2085
- );
2086
- }
2087
-
2088
- // Check for session tickets if allowSessionTicket is disabled
2089
- if (this.settings.allowSessionTicket === false && SniHandler.isClientHello(chunk)) {
2090
- // Analyze for session resumption attempt
2091
- const resumptionInfo = SniHandler.hasSessionResumption(
2092
- chunk,
2093
- this.settings.enableTlsDebugLogging
2094
- );
2095
-
2096
- if (resumptionInfo.isResumption) {
2097
- // Always log resumption attempt for easier debugging
2098
- // Try to extract SNI for logging
2099
- const extractedSNI = SniHandler.extractSNI(
2100
- chunk,
2101
- this.settings.enableTlsDebugLogging
2102
- );
2103
- console.log(
2104
- `[${connectionId}] Session resumption detected in SNI handler. ` +
2105
- `Has SNI: ${resumptionInfo.hasSNI ? 'Yes' : 'No'}, ` +
2106
- `SNI value: ${extractedSNI || 'None'}, ` +
2107
- `allowSessionTicket: ${this.settings.allowSessionTicket}`
2108
- );
2109
-
2110
- // Block if there's session resumption without SNI
2111
- if (!resumptionInfo.hasSNI) {
2112
- console.log(
2113
- `[${connectionId}] Session resumption detected in SNI handler without SNI and allowSessionTicket=false. ` +
2114
- `Terminating connection to force new TLS handshake.`
2115
- );
2116
- if (connectionRecord.incomingTerminationReason === null) {
2117
- connectionRecord.incomingTerminationReason = 'session_ticket_blocked';
2118
- this.incrementTerminationStat('incoming', 'session_ticket_blocked');
2119
- }
2120
- socket.end();
2121
- this.cleanupConnection(connectionRecord, 'session_ticket_blocked');
2122
- return;
2123
- } else {
2124
- if (this.settings.enableDetailedLogging) {
2125
- console.log(
2126
- `[${connectionId}] Session resumption with SNI detected in SNI handler. ` +
2127
- `Allowing connection since SNI is present.`
2128
- );
2129
- }
2130
- }
2131
- }
2132
- }
2133
-
2134
- // Create connection info object for SNI extraction
2135
- const connInfo = {
2136
- sourceIp: remoteIP,
2137
- sourcePort: socket.remotePort || 0,
2138
- destIp: socket.localAddress || '',
2139
- destPort: socket.localPort || 0,
2140
- };
2141
-
2142
- // Use the new processTlsPacket method for comprehensive handling
2143
- serverName =
2144
- SniHandler.processTlsPacket(
2145
- chunk,
2146
- connInfo,
2147
- this.settings.enableTlsDebugLogging,
2148
- connectionRecord.lockedDomain // Pass any previously negotiated domain as a hint
2149
- ) || '';
2150
- }
2151
-
2152
- // Lock the connection to the negotiated SNI.
2153
- connectionRecord.lockedDomain = serverName;
2154
-
2155
- if (this.settings.enableDetailedLogging) {
2156
- console.log(
2157
- `[${connectionId}] Received connection from ${remoteIP} with SNI: ${
2158
- serverName || '(empty)'
2159
- }`
2160
- );
2161
- }
2162
-
2163
- setupConnection(serverName, chunk);
2164
- });
2165
- } else {
2166
- initialDataReceived = true;
2167
- connectionRecord.hasReceivedInitialData = true;
2168
-
2169
- if (
2170
- this.settings.defaultAllowedIPs &&
2171
- this.settings.defaultAllowedIPs.length > 0 &&
2172
- !isAllowed(remoteIP, this.settings.defaultAllowedIPs)
2173
- ) {
2174
- return rejectIncomingConnection(
2175
- 'rejected',
2176
- `Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`
2177
- );
2178
- }
2179
-
2180
- setupConnection('');
2181
- }
2182
- }
2183
- };
2184
-
2185
- // --- SETUP LISTENERS ---
2186
- // Determine which ports to listen on.
2187
- const listeningPorts = new Set<number>();
2188
- if (this.settings.globalPortRanges && this.settings.globalPortRanges.length > 0) {
2189
- // Listen on every port defined by the global ranges.
2190
- for (const range of this.settings.globalPortRanges) {
2191
- for (let port = range.from; port <= range.to; port++) {
2192
- listeningPorts.add(port);
2193
- }
2194
- }
2195
- // Also ensure the default fromPort is listened to if it isn't already in the ranges.
2196
- listeningPorts.add(this.settings.fromPort);
2197
- } else {
2198
- listeningPorts.add(this.settings.fromPort);
2199
- }
2200
-
2201
- // Create a server for each port.
2202
- for (const port of listeningPorts) {
2203
- const server = plugins.net.createServer(connectionHandler).on('error', (err: Error) => {
2204
- console.log(`Server Error on port ${port}: ${err.message}`);
2205
- });
2206
- server.listen(port, () => {
2207
- const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
2208
- console.log(
2209
- `PortProxy -> OK: Now listening on port ${port}${
2210
- this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : ''
2211
- }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}`
2212
- );
2213
- });
2214
- this.netServers.push(server);
2215
- }
2216
-
2217
- // Log active connection count, longest running durations, and run parity checks periodically
2218
- this.connectionLogger = setInterval(() => {
2219
- // Immediately return if shutting down
2220
- if (this.isShuttingDown) return;
2221
-
2222
- const now = Date.now();
2223
- let maxIncoming = 0;
2224
- let maxOutgoing = 0;
2225
- let tlsConnections = 0;
2226
- let nonTlsConnections = 0;
2227
- let completedTlsHandshakes = 0;
2228
- let pendingTlsHandshakes = 0;
2229
- let keepAliveConnections = 0;
2230
- let networkProxyConnections = 0;
2231
- let domainSwitchedConnections = 0;
2232
-
2233
- // Create a copy of the keys to avoid modification during iteration
2234
- const connectionIds = [...this.connectionRecords.keys()];
2235
-
2236
- for (const id of connectionIds) {
2237
- const record = this.connectionRecords.get(id);
2238
- if (!record) continue;
2239
-
2240
- // Track connection stats
2241
- if (record.isTLS) {
2242
- tlsConnections++;
2243
- if (record.tlsHandshakeComplete) {
2244
- completedTlsHandshakes++;
2245
- } else {
2246
- pendingTlsHandshakes++;
2247
- }
2248
- } else {
2249
- nonTlsConnections++;
2250
- }
2251
-
2252
- if (record.hasKeepAlive) {
2253
- keepAliveConnections++;
2254
- }
2255
-
2256
- if (record.usingNetworkProxy) {
2257
- networkProxyConnections++;
2258
- }
2259
-
2260
- if (record.domainSwitches && record.domainSwitches > 0) {
2261
- domainSwitchedConnections++;
2262
- }
2263
-
2264
- maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime);
2265
- if (record.outgoingStartTime) {
2266
- maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime);
2267
- }
2268
- // Parity check: if outgoing socket closed and incoming remains active
2269
- if (
2270
- record.outgoingClosedTime &&
2271
- !record.incoming.destroyed &&
2272
- !record.connectionClosed &&
2273
- now - record.outgoingClosedTime > 120000
2274
- ) {
2275
- const remoteIP = record.remoteIP;
2276
- console.log(
2277
- `[${id}] Parity check: Incoming socket for ${remoteIP} still active ${plugins.prettyMs(
2278
- now - record.outgoingClosedTime
2279
- )} after outgoing closed.`
2280
- );
2281
- this.cleanupConnection(record, 'parity_check');
2282
- }
2283
-
2284
- // Check for stalled connections waiting for initial data
2285
- if (
2286
- !record.hasReceivedInitialData &&
2287
- now - record.incomingStartTime > this.settings.initialDataTimeout! / 2
2288
- ) {
2289
- console.log(
2290
- `[${id}] Warning: Connection from ${
2291
- record.remoteIP
2292
- } has not received initial data after ${plugins.prettyMs(
2293
- now - record.incomingStartTime
2294
- )}`
2295
- );
2296
- }
2297
-
2298
- // Skip inactivity check if disabled or for immortal keep-alive connections
2299
- if (
2300
- !this.settings.disableInactivityCheck &&
2301
- !(record.hasKeepAlive && this.settings.keepAliveTreatment === 'immortal')
2302
- ) {
2303
- const inactivityTime = now - record.lastActivity;
2304
-
2305
- // Use extended timeout for extended-treatment keep-alive connections
2306
- let effectiveTimeout = this.settings.inactivityTimeout!;
2307
- if (record.hasKeepAlive && this.settings.keepAliveTreatment === 'extended') {
2308
- const multiplier = this.settings.keepAliveInactivityMultiplier || 6;
2309
- effectiveTimeout = effectiveTimeout * multiplier;
2310
- }
2311
-
2312
- if (inactivityTime > effectiveTimeout && !record.connectionClosed) {
2313
- // For keep-alive connections, issue a warning first
2314
- if (record.hasKeepAlive && !record.inactivityWarningIssued) {
2315
- console.log(
2316
- `[${id}] Warning: Keep-alive connection from ${
2317
- record.remoteIP
2318
- } inactive for ${plugins.prettyMs(inactivityTime)}. ` +
2319
- `Will close in 10 minutes if no activity.`
2320
- );
2321
-
2322
- // Set warning flag and add grace period
2323
- record.inactivityWarningIssued = true;
2324
- record.lastActivity = now - (effectiveTimeout - 600000);
2325
-
2326
- // Try to stimulate activity with a probe packet
2327
- if (record.outgoing && !record.outgoing.destroyed) {
2328
- try {
2329
- record.outgoing.write(Buffer.alloc(0));
2330
-
2331
- if (this.settings.enableDetailedLogging) {
2332
- console.log(`[${id}] Sent probe packet to test keep-alive connection`);
2333
- }
2334
- } catch (err) {
2335
- console.log(`[${id}] Error sending probe packet: ${err}`);
2336
- }
2337
- }
2338
- } else {
2339
- // For non-keep-alive or after warning, close the connection
2340
- console.log(
2341
- `[${id}] Inactivity check: No activity on connection from ${record.remoteIP} ` +
2342
- `for ${plugins.prettyMs(inactivityTime)}.` +
2343
- (record.hasKeepAlive ? ' Despite keep-alive being enabled.' : '')
2344
- );
2345
- this.cleanupConnection(record, 'inactivity');
2346
- }
2347
- } else if (inactivityTime <= effectiveTimeout && record.inactivityWarningIssued) {
2348
- // If activity detected after warning, clear the warning
2349
- if (this.settings.enableDetailedLogging) {
2350
- console.log(
2351
- `[${id}] Connection activity detected after inactivity warning, resetting warning`
2352
- );
2353
- }
2354
- record.inactivityWarningIssued = false;
2355
- }
2356
- }
2357
- }
2358
-
2359
- // Log detailed stats periodically
2360
- console.log(
2361
- `Active connections: ${this.connectionRecords.size}. ` +
2362
- `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` +
2363
- `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}, ` +
2364
- `DomainSwitched=${domainSwitchedConnections}. ` +
2365
- `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(
2366
- maxOutgoing
2367
- )}. ` +
2368
- `Termination stats: ${JSON.stringify({
2369
- IN: this.terminationStats.incoming,
2370
- OUT: this.terminationStats.outgoing,
2371
- })}`
2372
- );
2373
- }, this.settings.inactivityCheckInterval || 60000);
2374
-
2375
- // Make sure the interval doesn't keep the process alive
2376
- if (this.connectionLogger.unref) {
2377
- this.connectionLogger.unref();
2378
- }
2379
- }
2380
-
2381
- /**
2382
- * Gracefully shut down the proxy
2383
- */
2384
- public async stop() {
2385
- console.log('PortProxy shutting down...');
2386
- this.isShuttingDown = true;
2387
-
2388
- // Stop accepting new connections
2389
- const closeServerPromises: Promise<void>[] = this.netServers.map(
2390
- (server) =>
2391
- new Promise<void>((resolve) => {
2392
- if (!server.listening) {
2393
- resolve();
2394
- return;
2395
- }
2396
- server.close((err) => {
2397
- if (err) {
2398
- console.log(`Error closing server: ${err.message}`);
2399
- }
2400
- resolve();
2401
- });
2402
- })
2403
- );
2404
-
2405
- // Stop the connection logger
2406
- if (this.connectionLogger) {
2407
- clearInterval(this.connectionLogger);
2408
- this.connectionLogger = null;
2409
- }
2410
-
2411
- // Wait for servers to close
2412
- await Promise.all(closeServerPromises);
2413
- console.log('All servers closed. Cleaning up active connections...');
2414
-
2415
- // Force destroy all active connections immediately
2416
- const connectionIds = [...this.connectionRecords.keys()];
2417
- console.log(`Cleaning up ${connectionIds.length} active connections...`);
2418
-
2419
- // First pass: End all connections gracefully
2420
- for (const id of connectionIds) {
2421
- const record = this.connectionRecords.get(id);
2422
- if (record) {
2423
- try {
2424
- // Clear any timers
2425
- if (record.cleanupTimer) {
2426
- clearTimeout(record.cleanupTimer);
2427
- record.cleanupTimer = undefined;
2428
- }
2429
-
2430
- // End sockets gracefully
2431
- if (record.incoming && !record.incoming.destroyed) {
2432
- record.incoming.end();
2433
- }
2434
-
2435
- if (record.outgoing && !record.outgoing.destroyed) {
2436
- record.outgoing.end();
2437
- }
2438
- } catch (err) {
2439
- console.log(`Error during graceful connection end for ${id}: ${err}`);
2440
- }
2441
- }
2442
- }
2443
-
2444
- // Short delay to allow graceful ends to process
2445
- await new Promise((resolve) => setTimeout(resolve, 100));
2446
-
2447
- // Second pass: Force destroy everything
2448
- for (const id of connectionIds) {
2449
- const record = this.connectionRecords.get(id);
2450
- if (record) {
2451
- try {
2452
- // Remove all listeners to prevent memory leaks
2453
- if (record.incoming) {
2454
- record.incoming.removeAllListeners();
2455
- if (!record.incoming.destroyed) {
2456
- record.incoming.destroy();
2457
- }
2458
- }
2459
-
2460
- if (record.outgoing) {
2461
- record.outgoing.removeAllListeners();
2462
- if (!record.outgoing.destroyed) {
2463
- record.outgoing.destroy();
2464
- }
2465
- }
2466
- } catch (err) {
2467
- console.log(`Error during forced connection destruction for ${id}: ${err}`);
2468
- }
2469
- }
2470
- }
2471
-
2472
- // Stop NetworkProxy if it was started (which also stops ACME manager)
2473
- if (this.networkProxy) {
2474
- try {
2475
- console.log('Stopping NetworkProxy...');
2476
- await this.networkProxy.stop();
2477
- console.log('NetworkProxy stopped successfully');
2478
-
2479
- // Log ACME shutdown if it was enabled
2480
- if (this.settings.acme?.enabled) {
2481
- console.log('ACME certificate manager stopped');
2482
- }
2483
- } catch (err) {
2484
- console.log(`Error stopping NetworkProxy: ${err}`);
2485
- }
2486
- }
2487
-
2488
- // Clear all tracking maps
2489
- this.connectionRecords.clear();
2490
- this.domainTargetIndices.clear();
2491
- this.connectionsByIP.clear();
2492
- this.connectionRateByIP.clear();
2493
- this.netServers = [];
2494
-
2495
- // Reset termination stats
2496
- this.terminationStats = {
2497
- incoming: {},
2498
- outgoing: {},
2499
- };
2500
-
2501
- console.log('PortProxy shutdown complete.');
2502
- }
2503
- }