@push.rocks/smartproxy 3.41.7 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/classes.portproxy.js +83 -69
  3. package/dist_ts/classes.pp.acmemanager.d.ts +34 -0
  4. package/dist_ts/classes.pp.acmemanager.js +123 -0
  5. package/dist_ts/classes.pp.connectionhandler.d.ts +39 -0
  6. package/dist_ts/classes.pp.connectionhandler.js +693 -0
  7. package/dist_ts/classes.pp.connectionmanager.d.ts +78 -0
  8. package/dist_ts/classes.pp.connectionmanager.js +378 -0
  9. package/dist_ts/classes.pp.domainconfigmanager.d.ts +55 -0
  10. package/dist_ts/classes.pp.domainconfigmanager.js +103 -0
  11. package/dist_ts/classes.pp.interfaces.d.ts +109 -0
  12. package/dist_ts/classes.pp.interfaces.js +2 -0
  13. package/dist_ts/classes.pp.networkproxybridge.d.ts +43 -0
  14. package/dist_ts/classes.pp.networkproxybridge.js +211 -0
  15. package/dist_ts/classes.pp.portproxy.d.ts +48 -0
  16. package/dist_ts/classes.pp.portproxy.js +268 -0
  17. package/dist_ts/classes.pp.portrangemanager.d.ts +56 -0
  18. package/dist_ts/classes.pp.portrangemanager.js +179 -0
  19. package/dist_ts/classes.pp.securitymanager.d.ts +47 -0
  20. package/dist_ts/classes.pp.securitymanager.js +126 -0
  21. package/dist_ts/classes.pp.snihandler.d.ts +160 -0
  22. package/dist_ts/classes.pp.snihandler.js +1073 -0
  23. package/dist_ts/classes.pp.timeoutmanager.d.ts +47 -0
  24. package/dist_ts/classes.pp.timeoutmanager.js +154 -0
  25. package/dist_ts/classes.pp.tlsmanager.d.ts +57 -0
  26. package/dist_ts/classes.pp.tlsmanager.js +132 -0
  27. package/dist_ts/index.d.ts +2 -2
  28. package/dist_ts/index.js +3 -3
  29. package/package.json +1 -1
  30. package/ts/00_commitinfo_data.ts +1 -1
  31. package/ts/classes.pp.acmemanager.ts +149 -0
  32. package/ts/classes.pp.connectionhandler.ts +982 -0
  33. package/ts/classes.pp.connectionmanager.ts +446 -0
  34. package/ts/classes.pp.domainconfigmanager.ts +123 -0
  35. package/ts/classes.pp.interfaces.ts +136 -0
  36. package/ts/classes.pp.networkproxybridge.ts +258 -0
  37. package/ts/classes.pp.portproxy.ts +344 -0
  38. package/ts/classes.pp.portrangemanager.ts +214 -0
  39. package/ts/classes.pp.securitymanager.ts +147 -0
  40. package/ts/{classes.snihandler.ts → classes.pp.snihandler.ts} +2 -169
  41. package/ts/classes.pp.timeoutmanager.ts +190 -0
  42. package/ts/classes.pp.tlsmanager.ts +206 -0
  43. package/ts/index.ts +2 -2
  44. package/ts/classes.portproxy.ts +0 -2496
@@ -0,0 +1,982 @@
1
+ import * as plugins from './plugins.js';
2
+ import type { IConnectionRecord, IDomainConfig, IPortProxySettings } from './classes.pp.interfaces.js';
3
+ import { ConnectionManager } from './classes.pp.connectionmanager.js';
4
+ import { SecurityManager } from './classes.pp.securitymanager.js';
5
+ import { DomainConfigManager } from './classes.pp.domainconfigmanager.js';
6
+ import { TlsManager } from './classes.pp.tlsmanager.js';
7
+ import { NetworkProxyBridge } from './classes.pp.networkproxybridge.js';
8
+ import { TimeoutManager } from './classes.pp.timeoutmanager.js';
9
+ import { PortRangeManager } from './classes.pp.portrangemanager.js';
10
+
11
+ /**
12
+ * Handles new connection processing and setup logic
13
+ */
14
+ export class ConnectionHandler {
15
+ constructor(
16
+ private settings: IPortProxySettings,
17
+ private connectionManager: ConnectionManager,
18
+ private securityManager: SecurityManager,
19
+ private domainConfigManager: DomainConfigManager,
20
+ private tlsManager: TlsManager,
21
+ private networkProxyBridge: NetworkProxyBridge,
22
+ private timeoutManager: TimeoutManager,
23
+ private portRangeManager: PortRangeManager
24
+ ) {}
25
+
26
+ /**
27
+ * Handle a new incoming connection
28
+ */
29
+ public handleConnection(socket: plugins.net.Socket): void {
30
+ const remoteIP = socket.remoteAddress || '';
31
+ const localPort = socket.localPort || 0;
32
+
33
+ // Validate IP against rate limits and connection limits
34
+ const ipValidation = this.securityManager.validateIP(remoteIP);
35
+ if (!ipValidation.allowed) {
36
+ console.log(`Connection rejected from ${remoteIP}: ${ipValidation.reason}`);
37
+ socket.end();
38
+ socket.destroy();
39
+ return;
40
+ }
41
+
42
+ // Create a new connection record
43
+ const record = this.connectionManager.createConnection(socket);
44
+ const connectionId = record.id;
45
+
46
+ // Apply socket optimizations
47
+ socket.setNoDelay(this.settings.noDelay);
48
+
49
+ // Apply keep-alive settings if enabled
50
+ if (this.settings.keepAlive) {
51
+ socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
52
+ record.hasKeepAlive = true;
53
+
54
+ // Apply enhanced TCP keep-alive options if enabled
55
+ if (this.settings.enableKeepAliveProbes) {
56
+ try {
57
+ // These are platform-specific and may not be available
58
+ if ('setKeepAliveProbes' in socket) {
59
+ (socket as any).setKeepAliveProbes(10);
60
+ }
61
+ if ('setKeepAliveInterval' in socket) {
62
+ (socket as any).setKeepAliveInterval(1000);
63
+ }
64
+ } catch (err) {
65
+ // Ignore errors - these are optional enhancements
66
+ if (this.settings.enableDetailedLogging) {
67
+ console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ if (this.settings.enableDetailedLogging) {
74
+ console.log(
75
+ `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
76
+ `Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
77
+ `Active connections: ${this.connectionManager.getConnectionCount()}`
78
+ );
79
+ } else {
80
+ console.log(
81
+ `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`
82
+ );
83
+ }
84
+
85
+ // Check if this connection should be forwarded directly to NetworkProxy
86
+ if (this.portRangeManager.shouldUseNetworkProxy(localPort)) {
87
+ this.handleNetworkProxyConnection(socket, record);
88
+ } else {
89
+ // For non-NetworkProxy ports, proceed with normal processing
90
+ this.handleStandardConnection(socket, record);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Handle a connection that should be forwarded to NetworkProxy
96
+ */
97
+ private handleNetworkProxyConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
98
+ const connectionId = record.id;
99
+ let initialDataReceived = false;
100
+
101
+ // Set an initial timeout for handshake data
102
+ let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
103
+ if (!initialDataReceived) {
104
+ console.log(
105
+ `[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
106
+ );
107
+
108
+ // Add a grace period instead of immediate termination
109
+ setTimeout(() => {
110
+ if (!initialDataReceived) {
111
+ console.log(`[${connectionId}] Final initial data timeout after grace period`);
112
+ if (record.incomingTerminationReason === null) {
113
+ record.incomingTerminationReason = 'initial_timeout';
114
+ this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
115
+ }
116
+ socket.end();
117
+ this.connectionManager.cleanupConnection(record, 'initial_timeout');
118
+ }
119
+ }, 30000); // 30 second grace period
120
+ }
121
+ }, this.settings.initialDataTimeout!);
122
+
123
+ // Make sure timeout doesn't keep the process alive
124
+ if (initialTimeout.unref) {
125
+ initialTimeout.unref();
126
+ }
127
+
128
+ // Set up error handler
129
+ socket.on('error', this.connectionManager.handleError('incoming', record));
130
+
131
+ // First data handler to capture initial TLS handshake for NetworkProxy
132
+ socket.once('data', (chunk: Buffer) => {
133
+ // Clear the initial timeout since we've received data
134
+ if (initialTimeout) {
135
+ clearTimeout(initialTimeout);
136
+ initialTimeout = null;
137
+ }
138
+
139
+ initialDataReceived = true;
140
+ record.hasReceivedInitialData = true;
141
+
142
+ // Block non-TLS connections on port 443
143
+ const localPort = record.localPort;
144
+ if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
145
+ console.log(
146
+ `[${connectionId}] Non-TLS connection detected on port 443. ` +
147
+ `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
148
+ );
149
+ if (record.incomingTerminationReason === null) {
150
+ record.incomingTerminationReason = 'non_tls_blocked';
151
+ this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
152
+ }
153
+ socket.end();
154
+ this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
155
+ return;
156
+ }
157
+
158
+ // Check if this looks like a TLS handshake
159
+ if (this.tlsManager.isTlsHandshake(chunk)) {
160
+ record.isTLS = true;
161
+
162
+ // Check session tickets if they're disabled
163
+ if (this.settings.allowSessionTicket === false && this.tlsManager.isClientHello(chunk)) {
164
+ // Create connection info for SNI extraction
165
+ const connInfo = {
166
+ sourceIp: record.remoteIP,
167
+ sourcePort: socket.remotePort || 0,
168
+ destIp: socket.localAddress || '',
169
+ destPort: socket.localPort || 0,
170
+ };
171
+
172
+ // Extract SNI for domain-specific NetworkProxy handling
173
+ const serverName = this.tlsManager.extractSNI(chunk, connInfo);
174
+
175
+ if (serverName) {
176
+ // If we got an SNI, check for domain-specific NetworkProxy settings
177
+ const domainConfig = this.domainConfigManager.findDomainConfig(serverName);
178
+
179
+ // Save domain config and SNI in connection record
180
+ record.domainConfig = domainConfig;
181
+ record.lockedDomain = serverName;
182
+
183
+ // Use domain-specific NetworkProxy port if configured
184
+ if (domainConfig && this.domainConfigManager.shouldUseNetworkProxy(domainConfig)) {
185
+ const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
186
+
187
+ if (this.settings.enableDetailedLogging) {
188
+ console.log(
189
+ `[${connectionId}] Using domain-specific NetworkProxy for ${serverName} on port ${networkProxyPort}`
190
+ );
191
+ }
192
+
193
+ // Forward to NetworkProxy with domain-specific port
194
+ this.networkProxyBridge.forwardToNetworkProxy(
195
+ connectionId,
196
+ socket,
197
+ record,
198
+ chunk,
199
+ networkProxyPort,
200
+ (reason) => this.connectionManager.initiateCleanupOnce(record, reason)
201
+ );
202
+ return;
203
+ }
204
+ }
205
+ }
206
+
207
+ // Forward directly to NetworkProxy without domain-specific settings
208
+ this.networkProxyBridge.forwardToNetworkProxy(
209
+ connectionId,
210
+ socket,
211
+ record,
212
+ chunk,
213
+ undefined,
214
+ (reason) => this.connectionManager.initiateCleanupOnce(record, reason)
215
+ );
216
+ } else {
217
+ // If not TLS, use normal direct connection
218
+ console.log(`[${connectionId}] Non-TLS connection on NetworkProxy port ${record.localPort}`);
219
+ this.setupDirectConnection(
220
+ socket,
221
+ record,
222
+ undefined,
223
+ undefined,
224
+ chunk
225
+ );
226
+ }
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Handle a standard (non-NetworkProxy) connection
232
+ */
233
+ private handleStandardConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
234
+ const connectionId = record.id;
235
+ const localPort = record.localPort;
236
+
237
+ // Define helpers for rejecting connections
238
+ const rejectIncomingConnection = (reason: string, logMessage: string) => {
239
+ console.log(`[${connectionId}] ${logMessage}`);
240
+ socket.end();
241
+ if (record.incomingTerminationReason === null) {
242
+ record.incomingTerminationReason = reason;
243
+ this.connectionManager.incrementTerminationStat('incoming', reason);
244
+ }
245
+ this.connectionManager.cleanupConnection(record, reason);
246
+ };
247
+
248
+ let initialDataReceived = false;
249
+
250
+ // Set an initial timeout for SNI data if needed
251
+ let initialTimeout: NodeJS.Timeout | null = null;
252
+ if (this.settings.sniEnabled) {
253
+ initialTimeout = setTimeout(() => {
254
+ if (!initialDataReceived) {
255
+ console.log(
256
+ `[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
257
+ );
258
+
259
+ // Add a grace period instead of immediate termination
260
+ setTimeout(() => {
261
+ if (!initialDataReceived) {
262
+ console.log(`[${connectionId}] Final initial data timeout after grace period`);
263
+ if (record.incomingTerminationReason === null) {
264
+ record.incomingTerminationReason = 'initial_timeout';
265
+ this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
266
+ }
267
+ socket.end();
268
+ this.connectionManager.cleanupConnection(record, 'initial_timeout');
269
+ }
270
+ }, 30000); // 30 second grace period
271
+ }
272
+ }, this.settings.initialDataTimeout!);
273
+
274
+ // Make sure timeout doesn't keep the process alive
275
+ if (initialTimeout.unref) {
276
+ initialTimeout.unref();
277
+ }
278
+ } else {
279
+ initialDataReceived = true;
280
+ record.hasReceivedInitialData = true;
281
+ }
282
+
283
+ socket.on('error', this.connectionManager.handleError('incoming', record));
284
+
285
+ // Track data for bytes counting
286
+ socket.on('data', (chunk: Buffer) => {
287
+ record.bytesReceived += chunk.length;
288
+ this.timeoutManager.updateActivity(record);
289
+
290
+ // Check for TLS handshake if this is the first chunk
291
+ if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
292
+ record.isTLS = true;
293
+
294
+ if (this.settings.enableTlsDebugLogging) {
295
+ console.log(
296
+ `[${connectionId}] TLS handshake detected from ${record.remoteIP}, ${chunk.length} bytes`
297
+ );
298
+ }
299
+ }
300
+ });
301
+
302
+ /**
303
+ * Sets up the connection to the target host.
304
+ */
305
+ const setupConnection = (
306
+ serverName: string,
307
+ initialChunk?: Buffer,
308
+ forcedDomain?: IDomainConfig,
309
+ overridePort?: number
310
+ ) => {
311
+ // Clear the initial timeout since we've received data
312
+ if (initialTimeout) {
313
+ clearTimeout(initialTimeout);
314
+ initialTimeout = null;
315
+ }
316
+
317
+ // Mark that we've received initial data
318
+ initialDataReceived = true;
319
+ record.hasReceivedInitialData = true;
320
+
321
+ // Check if this looks like a TLS handshake
322
+ if (initialChunk && this.tlsManager.isTlsHandshake(initialChunk)) {
323
+ record.isTLS = true;
324
+
325
+ if (this.settings.enableTlsDebugLogging) {
326
+ console.log(
327
+ `[${connectionId}] TLS handshake detected in setup, ${initialChunk.length} bytes`
328
+ );
329
+ }
330
+ }
331
+
332
+ // If a forcedDomain is provided (port-based routing), use it; otherwise, use SNI-based lookup.
333
+ const domainConfig = forcedDomain
334
+ ? forcedDomain
335
+ : serverName
336
+ ? this.domainConfigManager.findDomainConfig(serverName)
337
+ : undefined;
338
+
339
+ // Save domain config in connection record
340
+ record.domainConfig = domainConfig;
341
+
342
+ // Check if this domain should use NetworkProxy (domain-specific setting)
343
+ if (domainConfig &&
344
+ this.domainConfigManager.shouldUseNetworkProxy(domainConfig) &&
345
+ this.networkProxyBridge.getNetworkProxy()) {
346
+
347
+ if (this.settings.enableDetailedLogging) {
348
+ console.log(
349
+ `[${connectionId}] Domain ${serverName} is configured to use NetworkProxy`
350
+ );
351
+ }
352
+
353
+ const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
354
+
355
+ if (initialChunk && record.isTLS) {
356
+ // For TLS connections with initial chunk, forward to NetworkProxy
357
+ this.networkProxyBridge.forwardToNetworkProxy(
358
+ connectionId,
359
+ socket,
360
+ record,
361
+ initialChunk,
362
+ networkProxyPort,
363
+ (reason) => this.connectionManager.initiateCleanupOnce(record, reason)
364
+ );
365
+ return; // Skip normal connection setup
366
+ }
367
+ }
368
+
369
+ // IP validation
370
+ if (domainConfig) {
371
+ const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
372
+
373
+ // Skip IP validation if allowedIPs is empty
374
+ if (
375
+ domainConfig.allowedIPs.length > 0 &&
376
+ !this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)
377
+ ) {
378
+ return rejectIncomingConnection(
379
+ 'rejected',
380
+ `Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(
381
+ ', '
382
+ )}`
383
+ );
384
+ }
385
+ } else if (
386
+ this.settings.defaultAllowedIPs &&
387
+ this.settings.defaultAllowedIPs.length > 0
388
+ ) {
389
+ if (
390
+ !this.securityManager.isIPAuthorized(
391
+ record.remoteIP,
392
+ this.settings.defaultAllowedIPs,
393
+ this.settings.defaultBlockedIPs || []
394
+ )
395
+ ) {
396
+ return rejectIncomingConnection(
397
+ 'rejected',
398
+ `Connection rejected: IP ${record.remoteIP} not allowed by default allowed list`
399
+ );
400
+ }
401
+ }
402
+
403
+ // Save the initial SNI
404
+ if (serverName) {
405
+ record.lockedDomain = serverName;
406
+ }
407
+
408
+ // Set up the direct connection
409
+ this.setupDirectConnection(
410
+ socket,
411
+ record,
412
+ domainConfig,
413
+ serverName,
414
+ initialChunk,
415
+ overridePort
416
+ );
417
+ };
418
+
419
+ // --- PORT RANGE-BASED HANDLING ---
420
+ // Only apply port-based rules if the incoming port is within one of the global port ranges.
421
+ if (this.portRangeManager.isPortInGlobalRanges(localPort)) {
422
+ if (this.portRangeManager.shouldUseGlobalForwarding(localPort)) {
423
+ if (
424
+ this.settings.defaultAllowedIPs &&
425
+ this.settings.defaultAllowedIPs.length > 0 &&
426
+ !this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs)
427
+ ) {
428
+ console.log(
429
+ `[${connectionId}] Connection from ${record.remoteIP} rejected: IP ${record.remoteIP} not allowed in global default allowed list.`
430
+ );
431
+ socket.end();
432
+ return;
433
+ }
434
+ if (this.settings.enableDetailedLogging) {
435
+ console.log(
436
+ `[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} forwarded to global target IP ${this.settings.targetIP}.`
437
+ );
438
+ }
439
+ setupConnection(
440
+ '',
441
+ undefined,
442
+ {
443
+ domains: ['global'],
444
+ allowedIPs: this.settings.defaultAllowedIPs || [],
445
+ blockedIPs: this.settings.defaultBlockedIPs || [],
446
+ targetIPs: [this.settings.targetIP!],
447
+ portRanges: [],
448
+ },
449
+ localPort
450
+ );
451
+ return;
452
+ } else {
453
+ // Attempt to find a matching forced domain config based on the local port.
454
+ const forcedDomain = this.domainConfigManager.findDomainConfigForPort(localPort);
455
+
456
+ if (forcedDomain) {
457
+ const ipRules = this.domainConfigManager.getEffectiveIPRules(forcedDomain);
458
+
459
+ if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) {
460
+ console.log(
461
+ `[${connectionId}] Connection from ${record.remoteIP} rejected: IP not allowed for domain ${forcedDomain.domains.join(
462
+ ', '
463
+ )} on port ${localPort}.`
464
+ );
465
+ socket.end();
466
+ return;
467
+ }
468
+
469
+ if (this.settings.enableDetailedLogging) {
470
+ console.log(
471
+ `[${connectionId}] Port-based connection from ${record.remoteIP} on port ${localPort} matched domain ${forcedDomain.domains.join(
472
+ ', '
473
+ )}.`
474
+ );
475
+ }
476
+
477
+ setupConnection('', undefined, forcedDomain, localPort);
478
+ return;
479
+ }
480
+ // Fall through to SNI/default handling if no forced domain config is found.
481
+ }
482
+ }
483
+
484
+ // --- FALLBACK: SNI-BASED HANDLING (or default when SNI is disabled) ---
485
+ if (this.settings.sniEnabled) {
486
+ initialDataReceived = false;
487
+
488
+ socket.once('data', (chunk: Buffer) => {
489
+ // Clear timeout immediately
490
+ if (initialTimeout) {
491
+ clearTimeout(initialTimeout);
492
+ initialTimeout = null;
493
+ }
494
+
495
+ initialDataReceived = true;
496
+
497
+ // Block non-TLS connections on port 443
498
+ if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
499
+ console.log(
500
+ `[${connectionId}] Non-TLS connection detected on port 443 in SNI handler. ` +
501
+ `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
502
+ );
503
+ if (record.incomingTerminationReason === null) {
504
+ record.incomingTerminationReason = 'non_tls_blocked';
505
+ this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
506
+ }
507
+ socket.end();
508
+ this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
509
+ return;
510
+ }
511
+
512
+ // Try to extract SNI
513
+ let serverName = '';
514
+
515
+ if (this.tlsManager.isTlsHandshake(chunk)) {
516
+ record.isTLS = true;
517
+
518
+ if (this.settings.enableTlsDebugLogging) {
519
+ console.log(
520
+ `[${connectionId}] Extracting SNI from TLS handshake, ${chunk.length} bytes`
521
+ );
522
+ }
523
+
524
+ // Create connection info object for SNI extraction
525
+ const connInfo = {
526
+ sourceIp: record.remoteIP,
527
+ sourcePort: socket.remotePort || 0,
528
+ destIp: socket.localAddress || '',
529
+ destPort: socket.localPort || 0,
530
+ };
531
+
532
+ // Extract SNI
533
+ serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
534
+ }
535
+
536
+ // Lock the connection to the negotiated SNI.
537
+ record.lockedDomain = serverName;
538
+
539
+ if (this.settings.enableDetailedLogging) {
540
+ console.log(
541
+ `[${connectionId}] Received connection from ${record.remoteIP} with SNI: ${
542
+ serverName || '(empty)'
543
+ }`
544
+ );
545
+ }
546
+
547
+ setupConnection(serverName, chunk);
548
+ });
549
+ } else {
550
+ initialDataReceived = true;
551
+ record.hasReceivedInitialData = true;
552
+
553
+ if (
554
+ this.settings.defaultAllowedIPs &&
555
+ this.settings.defaultAllowedIPs.length > 0 &&
556
+ !this.securityManager.isIPAuthorized(record.remoteIP, this.settings.defaultAllowedIPs)
557
+ ) {
558
+ return rejectIncomingConnection(
559
+ 'rejected',
560
+ `Connection rejected: IP ${record.remoteIP} not allowed for non-SNI connection`
561
+ );
562
+ }
563
+
564
+ setupConnection('');
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Sets up a direct connection to the target
570
+ */
571
+ private setupDirectConnection(
572
+ socket: plugins.net.Socket,
573
+ record: IConnectionRecord,
574
+ domainConfig?: IDomainConfig,
575
+ serverName?: string,
576
+ initialChunk?: Buffer,
577
+ overridePort?: number
578
+ ): void {
579
+ const connectionId = record.id;
580
+
581
+ // Determine target host
582
+ const targetHost = domainConfig
583
+ ? this.domainConfigManager.getTargetIP(domainConfig)
584
+ : this.settings.targetIP!;
585
+
586
+ // Determine target port
587
+ const targetPort = overridePort !== undefined
588
+ ? overridePort
589
+ : this.settings.toPort;
590
+
591
+ // Setup connection options
592
+ const connectionOptions: plugins.net.NetConnectOpts = {
593
+ host: targetHost,
594
+ port: targetPort,
595
+ };
596
+
597
+ // Preserve source IP if configured
598
+ if (this.settings.preserveSourceIP) {
599
+ connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
600
+ }
601
+
602
+ // Create a safe queue for incoming data
603
+ const dataQueue: Buffer[] = [];
604
+ let queueSize = 0;
605
+ let processingQueue = false;
606
+ let drainPending = false;
607
+ let pipingEstablished = false;
608
+
609
+ // Pause the incoming socket to prevent buffer overflows
610
+ socket.pause();
611
+
612
+ // Function to safely process the data queue without losing events
613
+ const processDataQueue = () => {
614
+ if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
615
+
616
+ processingQueue = true;
617
+
618
+ try {
619
+ // Process all queued chunks with the current active handler
620
+ while (dataQueue.length > 0) {
621
+ const chunk = dataQueue.shift()!;
622
+ queueSize -= chunk.length;
623
+
624
+ // Once piping is established, we shouldn't get here,
625
+ // but just in case, pass to the outgoing socket directly
626
+ if (pipingEstablished && record.outgoing) {
627
+ record.outgoing.write(chunk);
628
+ continue;
629
+ }
630
+
631
+ // Track bytes received
632
+ record.bytesReceived += chunk.length;
633
+
634
+ // Check for TLS handshake
635
+ if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
636
+ record.isTLS = true;
637
+
638
+ if (this.settings.enableTlsDebugLogging) {
639
+ console.log(
640
+ `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
641
+ );
642
+ }
643
+ }
644
+
645
+ // Check if adding this chunk would exceed the buffer limit
646
+ const newSize = record.pendingDataSize + chunk.length;
647
+
648
+ if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
649
+ console.log(
650
+ `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
651
+ );
652
+ socket.end(); // Gracefully close the socket
653
+ this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded');
654
+ return;
655
+ }
656
+
657
+ // Buffer the chunk and update the size counter
658
+ record.pendingData.push(Buffer.from(chunk));
659
+ record.pendingDataSize = newSize;
660
+ this.timeoutManager.updateActivity(record);
661
+ }
662
+ } finally {
663
+ processingQueue = false;
664
+
665
+ // If there's a pending drain and we've processed everything,
666
+ // signal we're ready for more data if we haven't established piping yet
667
+ if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
668
+ drainPending = false;
669
+ socket.resume();
670
+ }
671
+ }
672
+ };
673
+
674
+ // Unified data handler that safely queues incoming data
675
+ const safeDataHandler = (chunk: Buffer) => {
676
+ // If piping is already established, just let the pipe handle it
677
+ if (pipingEstablished) return;
678
+
679
+ // Add to our queue for orderly processing
680
+ dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
681
+ queueSize += chunk.length;
682
+
683
+ // If queue is getting large, pause socket until we catch up
684
+ if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
685
+ socket.pause();
686
+ drainPending = true;
687
+ }
688
+
689
+ // Process the queue
690
+ processDataQueue();
691
+ };
692
+
693
+ // Add our safe data handler
694
+ socket.on('data', safeDataHandler);
695
+
696
+ // Add initial chunk to pending data if present
697
+ if (initialChunk) {
698
+ record.bytesReceived += initialChunk.length;
699
+ record.pendingData.push(Buffer.from(initialChunk));
700
+ record.pendingDataSize = initialChunk.length;
701
+ }
702
+
703
+ // Create the target socket but don't set up piping immediately
704
+ const targetSocket = plugins.net.connect(connectionOptions);
705
+ record.outgoing = targetSocket;
706
+ record.outgoingStartTime = Date.now();
707
+
708
+ // Apply socket optimizations
709
+ targetSocket.setNoDelay(this.settings.noDelay);
710
+
711
+ // Apply keep-alive settings to the outgoing connection as well
712
+ if (this.settings.keepAlive) {
713
+ targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
714
+
715
+ // Apply enhanced TCP keep-alive options if enabled
716
+ if (this.settings.enableKeepAliveProbes) {
717
+ try {
718
+ if ('setKeepAliveProbes' in targetSocket) {
719
+ (targetSocket as any).setKeepAliveProbes(10);
720
+ }
721
+ if ('setKeepAliveInterval' in targetSocket) {
722
+ (targetSocket as any).setKeepAliveInterval(1000);
723
+ }
724
+ } catch (err) {
725
+ // Ignore errors - these are optional enhancements
726
+ if (this.settings.enableDetailedLogging) {
727
+ console.log(
728
+ `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
729
+ );
730
+ }
731
+ }
732
+ }
733
+ }
734
+
735
+ // Setup specific error handler for connection phase
736
+ targetSocket.once('error', (err) => {
737
+ // This handler runs only once during the initial connection phase
738
+ const code = (err as any).code;
739
+ console.log(
740
+ `[${connectionId}] Connection setup error to ${targetHost}:${connectionOptions.port}: ${err.message} (${code})`
741
+ );
742
+
743
+ // Resume the incoming socket to prevent it from hanging
744
+ socket.resume();
745
+
746
+ if (code === 'ECONNREFUSED') {
747
+ console.log(
748
+ `[${connectionId}] Target ${targetHost}:${connectionOptions.port} refused connection`
749
+ );
750
+ } else if (code === 'ETIMEDOUT') {
751
+ console.log(
752
+ `[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} timed out`
753
+ );
754
+ } else if (code === 'ECONNRESET') {
755
+ console.log(
756
+ `[${connectionId}] Connection to ${targetHost}:${connectionOptions.port} was reset`
757
+ );
758
+ } else if (code === 'EHOSTUNREACH') {
759
+ console.log(`[${connectionId}] Host ${targetHost} is unreachable`);
760
+ }
761
+
762
+ // Clear any existing error handler after connection phase
763
+ targetSocket.removeAllListeners('error');
764
+
765
+ // Re-add the normal error handler for established connections
766
+ targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
767
+
768
+ if (record.outgoingTerminationReason === null) {
769
+ record.outgoingTerminationReason = 'connection_failed';
770
+ this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
771
+ }
772
+
773
+ // Clean up the connection
774
+ this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
775
+ });
776
+
777
+ // Setup close handler
778
+ targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
779
+ socket.on('close', this.connectionManager.handleClose('incoming', record));
780
+
781
+ // Handle timeouts with keep-alive awareness
782
+ socket.on('timeout', () => {
783
+ // For keep-alive connections, just log a warning instead of closing
784
+ if (record.hasKeepAlive) {
785
+ console.log(
786
+ `[${connectionId}] Timeout event on incoming keep-alive connection from ${
787
+ record.remoteIP
788
+ } after ${plugins.prettyMs(
789
+ this.settings.socketTimeout || 3600000
790
+ )}. Connection preserved.`
791
+ );
792
+ return;
793
+ }
794
+
795
+ // For non-keep-alive connections, proceed with normal cleanup
796
+ console.log(
797
+ `[${connectionId}] Timeout on incoming side from ${
798
+ record.remoteIP
799
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
800
+ );
801
+ if (record.incomingTerminationReason === null) {
802
+ record.incomingTerminationReason = 'timeout';
803
+ this.connectionManager.incrementTerminationStat('incoming', 'timeout');
804
+ }
805
+ this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming');
806
+ });
807
+
808
+ targetSocket.on('timeout', () => {
809
+ // For keep-alive connections, just log a warning instead of closing
810
+ if (record.hasKeepAlive) {
811
+ console.log(
812
+ `[${connectionId}] Timeout event on outgoing keep-alive connection from ${
813
+ record.remoteIP
814
+ } after ${plugins.prettyMs(
815
+ this.settings.socketTimeout || 3600000
816
+ )}. Connection preserved.`
817
+ );
818
+ return;
819
+ }
820
+
821
+ // For non-keep-alive connections, proceed with normal cleanup
822
+ console.log(
823
+ `[${connectionId}] Timeout on outgoing side from ${
824
+ record.remoteIP
825
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
826
+ );
827
+ if (record.outgoingTerminationReason === null) {
828
+ record.outgoingTerminationReason = 'timeout';
829
+ this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
830
+ }
831
+ this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing');
832
+ });
833
+
834
+ // Apply socket timeouts
835
+ this.timeoutManager.applySocketTimeouts(record);
836
+
837
+ // Track outgoing data for bytes counting
838
+ targetSocket.on('data', (chunk: Buffer) => {
839
+ record.bytesSent += chunk.length;
840
+ this.timeoutManager.updateActivity(record);
841
+ });
842
+
843
+ // Wait for the outgoing connection to be ready before setting up piping
844
+ targetSocket.once('connect', () => {
845
+ // Clear the initial connection error handler
846
+ targetSocket.removeAllListeners('error');
847
+
848
+ // Add the normal error handler for established connections
849
+ targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
850
+
851
+ // Process any remaining data in the queue before switching to piping
852
+ processDataQueue();
853
+
854
+ // Set up piping immediately
855
+ pipingEstablished = true;
856
+
857
+ // Flush all pending data to target
858
+ if (record.pendingData.length > 0) {
859
+ const combinedData = Buffer.concat(record.pendingData);
860
+
861
+ if (this.settings.enableDetailedLogging) {
862
+ console.log(`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`);
863
+ }
864
+
865
+ // Write pending data immediately
866
+ targetSocket.write(combinedData, (err) => {
867
+ if (err) {
868
+ console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
869
+ return this.connectionManager.initiateCleanupOnce(record, 'write_error');
870
+ }
871
+ });
872
+
873
+ // Clear the buffer now that we've processed it
874
+ record.pendingData = [];
875
+ record.pendingDataSize = 0;
876
+ }
877
+
878
+ // Setup piping in both directions without any delays
879
+ socket.pipe(targetSocket);
880
+ targetSocket.pipe(socket);
881
+
882
+ // Resume the socket to ensure data flows
883
+ socket.resume();
884
+
885
+ // Process any data that might be queued in the interim
886
+ if (dataQueue.length > 0) {
887
+ // Write any remaining queued data directly to the target socket
888
+ for (const chunk of dataQueue) {
889
+ targetSocket.write(chunk);
890
+ }
891
+ // Clear the queue
892
+ dataQueue.length = 0;
893
+ queueSize = 0;
894
+ }
895
+
896
+ if (this.settings.enableDetailedLogging) {
897
+ console.log(
898
+ `[${connectionId}] Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
899
+ `${
900
+ serverName
901
+ ? ` (SNI: ${serverName})`
902
+ : domainConfig
903
+ ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
904
+ : ''
905
+ }` +
906
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
907
+ record.hasKeepAlive ? 'Yes' : 'No'
908
+ }`
909
+ );
910
+ } else {
911
+ console.log(
912
+ `Connection established: ${record.remoteIP} -> ${targetHost}:${connectionOptions.port}` +
913
+ `${
914
+ serverName
915
+ ? ` (SNI: ${serverName})`
916
+ : domainConfig
917
+ ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
918
+ : ''
919
+ }`
920
+ );
921
+ }
922
+
923
+ // Add the renegotiation handler for SNI validation
924
+ if (serverName) {
925
+ // Create connection info object for the existing connection
926
+ const connInfo = {
927
+ sourceIp: record.remoteIP,
928
+ sourcePort: record.incoming.remotePort || 0,
929
+ destIp: record.incoming.localAddress || '',
930
+ destPort: record.incoming.localPort || 0,
931
+ };
932
+
933
+ // Create a renegotiation handler function
934
+ const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
935
+ connectionId,
936
+ serverName,
937
+ connInfo,
938
+ (connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
939
+ );
940
+
941
+ // Store the handler in the connection record so we can remove it during cleanup
942
+ record.renegotiationHandler = renegotiationHandler;
943
+
944
+ // Add the handler to the socket
945
+ socket.on('data', renegotiationHandler);
946
+
947
+ if (this.settings.enableDetailedLogging) {
948
+ console.log(
949
+ `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
950
+ );
951
+ if (this.settings.allowSessionTicket === false) {
952
+ console.log(
953
+ `[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
954
+ );
955
+ }
956
+ }
957
+ }
958
+
959
+ // Set connection timeout
960
+ record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(
961
+ record,
962
+ (record, reason) => {
963
+ console.log(
964
+ `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
965
+ );
966
+ this.connectionManager.initiateCleanupOnce(record, reason);
967
+ }
968
+ );
969
+
970
+ // Mark TLS handshake as complete for TLS connections
971
+ if (record.isTLS) {
972
+ record.tlsHandshakeComplete = true;
973
+
974
+ if (this.settings.enableTlsDebugLogging) {
975
+ console.log(
976
+ `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
977
+ );
978
+ }
979
+ }
980
+ });
981
+ }
982
+ }