@push.rocks/smartproxy 13.1.2 → 15.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 (29) hide show
  1. package/dist_ts/00_commitinfo_data.js +3 -3
  2. package/dist_ts/proxies/smart-proxy/index.d.ts +5 -3
  3. package/dist_ts/proxies/smart-proxy/index.js +9 -5
  4. package/dist_ts/proxies/smart-proxy/models/index.d.ts +2 -0
  5. package/dist_ts/proxies/smart-proxy/models/index.js +2 -1
  6. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +82 -15
  7. package/dist_ts/proxies/smart-proxy/models/interfaces.js +10 -1
  8. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +133 -0
  9. package/dist_ts/proxies/smart-proxy/models/route-types.js +2 -0
  10. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +55 -0
  11. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +804 -0
  12. package/dist_ts/proxies/smart-proxy/route-helpers.d.ts +127 -0
  13. package/dist_ts/proxies/smart-proxy/route-helpers.js +196 -0
  14. package/dist_ts/proxies/smart-proxy/route-manager.d.ts +103 -0
  15. package/dist_ts/proxies/smart-proxy/route-manager.js +483 -0
  16. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +19 -8
  17. package/dist_ts/proxies/smart-proxy/smart-proxy.js +239 -46
  18. package/package.json +2 -2
  19. package/readme.md +863 -423
  20. package/readme.plan.md +311 -250
  21. package/ts/00_commitinfo_data.ts +2 -2
  22. package/ts/proxies/smart-proxy/index.ts +20 -4
  23. package/ts/proxies/smart-proxy/models/index.ts +4 -0
  24. package/ts/proxies/smart-proxy/models/interfaces.ts +91 -13
  25. package/ts/proxies/smart-proxy/models/route-types.ts +184 -0
  26. package/ts/proxies/smart-proxy/route-connection-handler.ts +1117 -0
  27. package/ts/proxies/smart-proxy/route-helpers.ts +344 -0
  28. package/ts/proxies/smart-proxy/route-manager.ts +587 -0
  29. package/ts/proxies/smart-proxy/smart-proxy.ts +300 -69
@@ -0,0 +1,1117 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type {
3
+ IConnectionRecord,
4
+ IDomainConfig,
5
+ ISmartProxyOptions
6
+ } from './models/interfaces.js';
7
+ import {
8
+ isRoutedOptions,
9
+ isLegacyOptions
10
+ } from './models/interfaces.js';
11
+ import type {
12
+ IRouteConfig,
13
+ IRouteAction
14
+ } from './models/route-types.js';
15
+ import { ConnectionManager } from './connection-manager.js';
16
+ import { SecurityManager } from './security-manager.js';
17
+ import { DomainConfigManager } from './domain-config-manager.js';
18
+ import { TlsManager } from './tls-manager.js';
19
+ import { NetworkProxyBridge } from './network-proxy-bridge.js';
20
+ import { TimeoutManager } from './timeout-manager.js';
21
+ import { RouteManager } from './route-manager.js';
22
+ import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
23
+ import type { TForwardingType } from '../../forwarding/config/forwarding-types.js';
24
+
25
+ /**
26
+ * Handles new connection processing and setup logic with support for route-based configuration
27
+ */
28
+ export class RouteConnectionHandler {
29
+ private settings: ISmartProxyOptions;
30
+
31
+ constructor(
32
+ settings: ISmartProxyOptions,
33
+ private connectionManager: ConnectionManager,
34
+ private securityManager: SecurityManager,
35
+ private domainConfigManager: DomainConfigManager,
36
+ private tlsManager: TlsManager,
37
+ private networkProxyBridge: NetworkProxyBridge,
38
+ private timeoutManager: TimeoutManager,
39
+ private routeManager: RouteManager
40
+ ) {
41
+ this.settings = settings;
42
+ }
43
+
44
+ /**
45
+ * Handle a new incoming connection
46
+ */
47
+ public handleConnection(socket: plugins.net.Socket): void {
48
+ const remoteIP = socket.remoteAddress || '';
49
+ const localPort = socket.localPort || 0;
50
+
51
+ // Validate IP against rate limits and connection limits
52
+ const ipValidation = this.securityManager.validateIP(remoteIP);
53
+ if (!ipValidation.allowed) {
54
+ console.log(`Connection rejected from ${remoteIP}: ${ipValidation.reason}`);
55
+ socket.end();
56
+ socket.destroy();
57
+ return;
58
+ }
59
+
60
+ // Create a new connection record
61
+ const record = this.connectionManager.createConnection(socket);
62
+ const connectionId = record.id;
63
+
64
+ // Apply socket optimizations
65
+ socket.setNoDelay(this.settings.noDelay);
66
+
67
+ // Apply keep-alive settings if enabled
68
+ if (this.settings.keepAlive) {
69
+ socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
70
+ record.hasKeepAlive = true;
71
+
72
+ // Apply enhanced TCP keep-alive options if enabled
73
+ if (this.settings.enableKeepAliveProbes) {
74
+ try {
75
+ // These are platform-specific and may not be available
76
+ if ('setKeepAliveProbes' in socket) {
77
+ (socket as any).setKeepAliveProbes(10);
78
+ }
79
+ if ('setKeepAliveInterval' in socket) {
80
+ (socket as any).setKeepAliveInterval(1000);
81
+ }
82
+ } catch (err) {
83
+ // Ignore errors - these are optional enhancements
84
+ if (this.settings.enableDetailedLogging) {
85
+ console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ if (this.settings.enableDetailedLogging) {
92
+ console.log(
93
+ `[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
94
+ `Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
95
+ `Active connections: ${this.connectionManager.getConnectionCount()}`
96
+ );
97
+ } else {
98
+ console.log(
99
+ `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`
100
+ );
101
+ }
102
+
103
+ // Start TLS SNI handling
104
+ this.handleTlsConnection(socket, record);
105
+ }
106
+
107
+ /**
108
+ * Handle a connection and wait for TLS handshake for SNI extraction if needed
109
+ */
110
+ private handleTlsConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
111
+ const connectionId = record.id;
112
+ const localPort = record.localPort;
113
+ let initialDataReceived = false;
114
+
115
+ // Set an initial timeout for handshake data
116
+ let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
117
+ if (!initialDataReceived) {
118
+ console.log(
119
+ `[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
120
+ );
121
+
122
+ // Add a grace period
123
+ setTimeout(() => {
124
+ if (!initialDataReceived) {
125
+ console.log(`[${connectionId}] Final initial data timeout after grace period`);
126
+ if (record.incomingTerminationReason === null) {
127
+ record.incomingTerminationReason = 'initial_timeout';
128
+ this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
129
+ }
130
+ socket.end();
131
+ this.connectionManager.cleanupConnection(record, 'initial_timeout');
132
+ }
133
+ }, 30000);
134
+ }
135
+ }, this.settings.initialDataTimeout!);
136
+
137
+ // Make sure timeout doesn't keep the process alive
138
+ if (initialTimeout.unref) {
139
+ initialTimeout.unref();
140
+ }
141
+
142
+ // Set up error handler
143
+ socket.on('error', this.connectionManager.handleError('incoming', record));
144
+
145
+ // First data handler to capture initial TLS handshake
146
+ socket.once('data', (chunk: Buffer) => {
147
+ // Clear the initial timeout since we've received data
148
+ if (initialTimeout) {
149
+ clearTimeout(initialTimeout);
150
+ initialTimeout = null;
151
+ }
152
+
153
+ initialDataReceived = true;
154
+ record.hasReceivedInitialData = true;
155
+
156
+ // Block non-TLS connections on port 443
157
+ if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
158
+ console.log(
159
+ `[${connectionId}] Non-TLS connection detected on port 443. ` +
160
+ `Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
161
+ );
162
+ if (record.incomingTerminationReason === null) {
163
+ record.incomingTerminationReason = 'non_tls_blocked';
164
+ this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
165
+ }
166
+ socket.end();
167
+ this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
168
+ return;
169
+ }
170
+
171
+ // Check if this looks like a TLS handshake
172
+ let serverName = '';
173
+ if (this.tlsManager.isTlsHandshake(chunk)) {
174
+ record.isTLS = true;
175
+
176
+ // Check for ClientHello to extract SNI
177
+ if (this.tlsManager.isClientHello(chunk)) {
178
+ // Create connection info for SNI extraction
179
+ const connInfo = {
180
+ sourceIp: record.remoteIP,
181
+ sourcePort: socket.remotePort || 0,
182
+ destIp: socket.localAddress || '',
183
+ destPort: socket.localPort || 0,
184
+ };
185
+
186
+ // Extract SNI
187
+ serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
188
+
189
+ // Lock the connection to the negotiated SNI
190
+ record.lockedDomain = serverName;
191
+
192
+ // Check if we should reject connections without SNI
193
+ if (!serverName && this.settings.allowSessionTicket === false) {
194
+ console.log(`[${connectionId}] No SNI detected in TLS ClientHello; sending TLS alert.`);
195
+ if (record.incomingTerminationReason === null) {
196
+ record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
197
+ this.connectionManager.incrementTerminationStat('incoming', 'session_ticket_blocked_no_sni');
198
+ }
199
+ const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
200
+ try {
201
+ socket.cork();
202
+ socket.write(alert);
203
+ socket.uncork();
204
+ socket.end();
205
+ } catch {
206
+ socket.end();
207
+ }
208
+ this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
209
+ return;
210
+ }
211
+
212
+ if (this.settings.enableDetailedLogging) {
213
+ console.log(`[${connectionId}] TLS connection with SNI: ${serverName || '(empty)'}`);
214
+ }
215
+ }
216
+ }
217
+
218
+ // Find the appropriate route for this connection
219
+ this.routeConnection(socket, record, serverName, chunk);
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Route the connection based on match criteria
225
+ */
226
+ private routeConnection(
227
+ socket: plugins.net.Socket,
228
+ record: IConnectionRecord,
229
+ serverName: string,
230
+ initialChunk?: Buffer
231
+ ): void {
232
+ const connectionId = record.id;
233
+ const localPort = record.localPort;
234
+ const remoteIP = record.remoteIP;
235
+
236
+ // Find matching route
237
+ const routeMatch = this.routeManager.findMatchingRoute({
238
+ port: localPort,
239
+ domain: serverName,
240
+ clientIp: remoteIP,
241
+ path: undefined, // We don't have path info at this point
242
+ tlsVersion: undefined // We don't extract TLS version yet
243
+ });
244
+
245
+ if (!routeMatch) {
246
+ console.log(`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`);
247
+
248
+ // Fall back to legacy matching if we're using a hybrid configuration
249
+ const domainConfig = serverName
250
+ ? this.domainConfigManager.findDomainConfig(serverName)
251
+ : this.domainConfigManager.findDomainConfigForPort(localPort);
252
+
253
+ if (domainConfig) {
254
+ if (this.settings.enableDetailedLogging) {
255
+ console.log(`[${connectionId}] Using legacy domain configuration for ${serverName || 'port ' + localPort}`);
256
+ }
257
+
258
+ // Associate this domain config with the connection
259
+ record.domainConfig = domainConfig;
260
+
261
+ // Handle the connection using the legacy setup
262
+ return this.handleLegacyConnection(socket, record, serverName, domainConfig, initialChunk);
263
+ }
264
+
265
+ // No matching route or domain config, use default/fallback handling
266
+ console.log(`[${connectionId}] Using default route handling for connection`);
267
+
268
+ // Check default security settings
269
+ const defaultSecuritySettings = this.settings.defaults?.security;
270
+ if (defaultSecuritySettings) {
271
+ if (defaultSecuritySettings.allowedIPs && defaultSecuritySettings.allowedIPs.length > 0) {
272
+ const isAllowed = this.securityManager.isIPAuthorized(
273
+ remoteIP,
274
+ defaultSecuritySettings.allowedIPs,
275
+ defaultSecuritySettings.blockedIPs || []
276
+ );
277
+
278
+ if (!isAllowed) {
279
+ console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`);
280
+ socket.end();
281
+ this.connectionManager.cleanupConnection(record, 'ip_blocked');
282
+ return;
283
+ }
284
+ }
285
+ } else if (this.settings.defaultAllowedIPs && this.settings.defaultAllowedIPs.length > 0) {
286
+ // Legacy default IP restrictions
287
+ const isAllowed = this.securityManager.isIPAuthorized(
288
+ remoteIP,
289
+ this.settings.defaultAllowedIPs,
290
+ this.settings.defaultBlockedIPs || []
291
+ );
292
+
293
+ if (!isAllowed) {
294
+ console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`);
295
+ socket.end();
296
+ this.connectionManager.cleanupConnection(record, 'ip_blocked');
297
+ return;
298
+ }
299
+ }
300
+
301
+ // Setup direct connection with default settings
302
+ let targetHost: string;
303
+ let targetPort: number;
304
+
305
+ if (isRoutedOptions(this.settings) && this.settings.defaults?.target) {
306
+ // Use defaults from routed configuration
307
+ targetHost = this.settings.defaults.target.host;
308
+ targetPort = this.settings.defaults.target.port;
309
+ } else {
310
+ // Fall back to legacy settings
311
+ targetHost = this.settings.targetIP || 'localhost';
312
+ targetPort = this.settings.toPort;
313
+ }
314
+
315
+ return this.setupDirectConnection(
316
+ socket,
317
+ record,
318
+ undefined,
319
+ serverName,
320
+ initialChunk,
321
+ undefined,
322
+ targetHost,
323
+ targetPort
324
+ );
325
+ }
326
+
327
+ // A matching route was found
328
+ const route = routeMatch.route;
329
+
330
+ if (this.settings.enableDetailedLogging) {
331
+ console.log(
332
+ `[${connectionId}] Route matched: "${route.name || 'unnamed'}" for ${serverName || 'connection'} on port ${localPort}`
333
+ );
334
+ }
335
+
336
+ // Handle the route based on its action type
337
+ switch (route.action.type) {
338
+ case 'forward':
339
+ return this.handleForwardAction(socket, record, route, initialChunk);
340
+
341
+ case 'redirect':
342
+ return this.handleRedirectAction(socket, record, route);
343
+
344
+ case 'block':
345
+ return this.handleBlockAction(socket, record, route);
346
+
347
+ default:
348
+ console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`);
349
+ socket.end();
350
+ this.connectionManager.cleanupConnection(record, 'unknown_action');
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Handle a forward action for a route
356
+ */
357
+ private handleForwardAction(
358
+ socket: plugins.net.Socket,
359
+ record: IConnectionRecord,
360
+ route: IRouteConfig,
361
+ initialChunk?: Buffer
362
+ ): void {
363
+ const connectionId = record.id;
364
+ const action = route.action;
365
+
366
+ // We should have a target configuration for forwarding
367
+ if (!action.target) {
368
+ console.log(`[${connectionId}] Forward action missing target configuration`);
369
+ socket.end();
370
+ this.connectionManager.cleanupConnection(record, 'missing_target');
371
+ return;
372
+ }
373
+
374
+ // Determine if this needs TLS handling
375
+ if (action.tls) {
376
+ switch (action.tls.mode) {
377
+ case 'passthrough':
378
+ // For TLS passthrough, just forward directly
379
+ if (this.settings.enableDetailedLogging) {
380
+ console.log(`[${connectionId}] Using TLS passthrough to ${action.target.host}`);
381
+ }
382
+
383
+ // Allow for array of hosts
384
+ const targetHost = Array.isArray(action.target.host)
385
+ ? action.target.host[Math.floor(Math.random() * action.target.host.length)]
386
+ : action.target.host;
387
+
388
+ // Determine target port - either target port or preserve incoming port
389
+ const targetPort = action.target.preservePort ? record.localPort : action.target.port;
390
+
391
+ return this.setupDirectConnection(
392
+ socket,
393
+ record,
394
+ undefined,
395
+ record.lockedDomain,
396
+ initialChunk,
397
+ undefined,
398
+ targetHost,
399
+ targetPort
400
+ );
401
+
402
+ case 'terminate':
403
+ case 'terminate-and-reencrypt':
404
+ // For TLS termination, use NetworkProxy
405
+ if (this.networkProxyBridge.getNetworkProxy()) {
406
+ if (this.settings.enableDetailedLogging) {
407
+ console.log(
408
+ `[${connectionId}] Using NetworkProxy for TLS termination to ${action.target.host}`
409
+ );
410
+ }
411
+
412
+ // If we have an initial chunk with TLS data, start processing it
413
+ if (initialChunk && record.isTLS) {
414
+ return this.networkProxyBridge.forwardToNetworkProxy(
415
+ connectionId,
416
+ socket,
417
+ record,
418
+ initialChunk,
419
+ this.settings.networkProxyPort,
420
+ (reason) => this.connectionManager.initiateCleanupOnce(record, reason)
421
+ );
422
+ }
423
+
424
+ // This shouldn't normally happen - we should have TLS data at this point
425
+ console.log(`[${connectionId}] TLS termination route without TLS data`);
426
+ socket.end();
427
+ this.connectionManager.cleanupConnection(record, 'tls_error');
428
+ return;
429
+ } else {
430
+ console.log(`[${connectionId}] NetworkProxy not available for TLS termination`);
431
+ socket.end();
432
+ this.connectionManager.cleanupConnection(record, 'no_network_proxy');
433
+ return;
434
+ }
435
+ }
436
+ } else {
437
+ // No TLS settings - basic forwarding
438
+ if (this.settings.enableDetailedLogging) {
439
+ console.log(`[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`);
440
+ }
441
+
442
+ // Allow for array of hosts
443
+ const targetHost = Array.isArray(action.target.host)
444
+ ? action.target.host[Math.floor(Math.random() * action.target.host.length)]
445
+ : action.target.host;
446
+
447
+ // Determine target port - either target port or preserve incoming port
448
+ const targetPort = action.target.preservePort ? record.localPort : action.target.port;
449
+
450
+ return this.setupDirectConnection(
451
+ socket,
452
+ record,
453
+ undefined,
454
+ record.lockedDomain,
455
+ initialChunk,
456
+ undefined,
457
+ targetHost,
458
+ targetPort
459
+ );
460
+ }
461
+ }
462
+
463
+ /**
464
+ * Handle a redirect action for a route
465
+ */
466
+ private handleRedirectAction(
467
+ socket: plugins.net.Socket,
468
+ record: IConnectionRecord,
469
+ route: IRouteConfig
470
+ ): void {
471
+ const connectionId = record.id;
472
+ const action = route.action;
473
+
474
+ // We should have a redirect configuration
475
+ if (!action.redirect) {
476
+ console.log(`[${connectionId}] Redirect action missing redirect configuration`);
477
+ socket.end();
478
+ this.connectionManager.cleanupConnection(record, 'missing_redirect');
479
+ return;
480
+ }
481
+
482
+ // For TLS connections, we can't do redirects at the TCP level
483
+ if (record.isTLS) {
484
+ console.log(`[${connectionId}] Cannot redirect TLS connection at TCP level`);
485
+ socket.end();
486
+ this.connectionManager.cleanupConnection(record, 'tls_redirect_error');
487
+ return;
488
+ }
489
+
490
+ // Wait for the first HTTP request to perform the redirect
491
+ const dataListeners: ((chunk: Buffer) => void)[] = [];
492
+
493
+ const httpDataHandler = (chunk: Buffer) => {
494
+ // Remove all data listeners to avoid duplicated processing
495
+ for (const listener of dataListeners) {
496
+ socket.removeListener('data', listener);
497
+ }
498
+
499
+ // Parse HTTP request to get path
500
+ try {
501
+ const headersEnd = chunk.indexOf('\r\n\r\n');
502
+ if (headersEnd === -1) {
503
+ // Not a complete HTTP request, need more data
504
+ socket.once('data', httpDataHandler);
505
+ dataListeners.push(httpDataHandler);
506
+ return;
507
+ }
508
+
509
+ const httpHeaders = chunk.slice(0, headersEnd).toString();
510
+ const requestLine = httpHeaders.split('\r\n')[0];
511
+ const [method, path] = requestLine.split(' ');
512
+
513
+ // Extract Host header
514
+ const hostMatch = httpHeaders.match(/Host: (.+?)(\r\n|\r|\n|$)/i);
515
+ const host = hostMatch ? hostMatch[1].trim() : record.lockedDomain || '';
516
+
517
+ // Process the redirect URL with template variables
518
+ let redirectUrl = action.redirect.to;
519
+ redirectUrl = redirectUrl.replace(/\{domain\}/g, host);
520
+ redirectUrl = redirectUrl.replace(/\{path\}/g, path || '');
521
+ redirectUrl = redirectUrl.replace(/\{port\}/g, record.localPort.toString());
522
+
523
+ // Prepare the HTTP redirect response
524
+ const redirectResponse = [
525
+ `HTTP/1.1 ${action.redirect.status} Moved`,
526
+ `Location: ${redirectUrl}`,
527
+ 'Connection: close',
528
+ 'Content-Length: 0',
529
+ '',
530
+ ''
531
+ ].join('\r\n');
532
+
533
+ if (this.settings.enableDetailedLogging) {
534
+ console.log(`[${connectionId}] Redirecting to ${redirectUrl} with status ${action.redirect.status}`);
535
+ }
536
+
537
+ // Send the redirect response
538
+ socket.end(redirectResponse);
539
+ this.connectionManager.initiateCleanupOnce(record, 'redirect_complete');
540
+ } catch (err) {
541
+ console.log(`[${connectionId}] Error processing HTTP redirect: ${err}`);
542
+ socket.end();
543
+ this.connectionManager.initiateCleanupOnce(record, 'redirect_error');
544
+ }
545
+ };
546
+
547
+ // Setup the HTTP data handler
548
+ socket.once('data', httpDataHandler);
549
+ dataListeners.push(httpDataHandler);
550
+ }
551
+
552
+ /**
553
+ * Handle a block action for a route
554
+ */
555
+ private handleBlockAction(
556
+ socket: plugins.net.Socket,
557
+ record: IConnectionRecord,
558
+ route: IRouteConfig
559
+ ): void {
560
+ const connectionId = record.id;
561
+
562
+ if (this.settings.enableDetailedLogging) {
563
+ console.log(`[${connectionId}] Blocking connection based on route "${route.name || 'unnamed'}"`);
564
+ }
565
+
566
+ // Simply close the connection
567
+ socket.end();
568
+ this.connectionManager.initiateCleanupOnce(record, 'route_blocked');
569
+ }
570
+
571
+ /**
572
+ * Handle a connection using legacy domain configuration
573
+ */
574
+ private handleLegacyConnection(
575
+ socket: plugins.net.Socket,
576
+ record: IConnectionRecord,
577
+ serverName: string,
578
+ domainConfig: IDomainConfig,
579
+ initialChunk?: Buffer
580
+ ): void {
581
+ const connectionId = record.id;
582
+
583
+ // Get the forwarding type for this domain
584
+ const forwardingType = this.domainConfigManager.getForwardingType(domainConfig);
585
+
586
+ // IP validation
587
+ const ipRules = this.domainConfigManager.getEffectiveIPRules(domainConfig);
588
+
589
+ if (!this.securityManager.isIPAuthorized(record.remoteIP, ipRules.allowedIPs, ipRules.blockedIPs)) {
590
+ console.log(
591
+ `[${connectionId}] Connection rejected: IP ${record.remoteIP} not allowed for domain ${domainConfig.domains.join(', ')}`
592
+ );
593
+ socket.end();
594
+ this.connectionManager.initiateCleanupOnce(record, 'ip_blocked');
595
+ return;
596
+ }
597
+
598
+ // Handle based on forwarding type
599
+ switch (forwardingType) {
600
+ case 'http-only':
601
+ // For HTTP-only configs with TLS traffic
602
+ if (record.isTLS) {
603
+ console.log(`[${connectionId}] Received TLS connection for HTTP-only domain ${serverName}`);
604
+ socket.end();
605
+ this.connectionManager.initiateCleanupOnce(record, 'wrong_protocol');
606
+ return;
607
+ }
608
+ break;
609
+
610
+ case 'https-passthrough':
611
+ // For TLS passthrough with TLS traffic
612
+ if (record.isTLS) {
613
+ try {
614
+ const handler = this.domainConfigManager.getForwardingHandler(domainConfig);
615
+
616
+ if (this.settings.enableDetailedLogging) {
617
+ console.log(`[${connectionId}] Using forwarding handler for SNI passthrough to ${serverName}`);
618
+ }
619
+
620
+ // Handle the connection using the handler
621
+ return handler.handleConnection(socket);
622
+ } catch (err) {
623
+ console.log(`[${connectionId}] Error using forwarding handler: ${err}`);
624
+ }
625
+ }
626
+ break;
627
+
628
+ case 'https-terminate-to-http':
629
+ case 'https-terminate-to-https':
630
+ // For TLS termination with TLS traffic
631
+ if (record.isTLS) {
632
+ const networkProxyPort = this.domainConfigManager.getNetworkProxyPort(domainConfig);
633
+
634
+ if (this.settings.enableDetailedLogging) {
635
+ console.log(`[${connectionId}] Using TLS termination (${forwardingType}) for ${serverName} on port ${networkProxyPort}`);
636
+ }
637
+
638
+ // Forward to NetworkProxy with domain-specific port
639
+ return this.networkProxyBridge.forwardToNetworkProxy(
640
+ connectionId,
641
+ socket,
642
+ record,
643
+ initialChunk!,
644
+ networkProxyPort,
645
+ (reason) => this.connectionManager.initiateCleanupOnce(record, reason)
646
+ );
647
+ }
648
+ break;
649
+ }
650
+
651
+ // If we're still here, use the forwarding handler if available
652
+ try {
653
+ const handler = this.domainConfigManager.getForwardingHandler(domainConfig);
654
+
655
+ if (this.settings.enableDetailedLogging) {
656
+ console.log(`[${connectionId}] Using general forwarding handler for domain ${serverName || 'unknown'}`);
657
+ }
658
+
659
+ // Handle the connection using the handler
660
+ return handler.handleConnection(socket);
661
+ } catch (err) {
662
+ console.log(`[${connectionId}] Error using forwarding handler: ${err}`);
663
+ }
664
+
665
+ // Fallback: set up direct connection
666
+ const targetIp = this.domainConfigManager.getTargetIP(domainConfig);
667
+ const targetPort = this.domainConfigManager.getTargetPort(domainConfig, this.settings.toPort);
668
+
669
+ return this.setupDirectConnection(
670
+ socket,
671
+ record,
672
+ domainConfig,
673
+ serverName,
674
+ initialChunk,
675
+ undefined,
676
+ targetIp,
677
+ targetPort
678
+ );
679
+ }
680
+
681
+ /**
682
+ * Sets up a direct connection to the target
683
+ */
684
+ private setupDirectConnection(
685
+ socket: plugins.net.Socket,
686
+ record: IConnectionRecord,
687
+ domainConfig?: IDomainConfig,
688
+ serverName?: string,
689
+ initialChunk?: Buffer,
690
+ overridePort?: number,
691
+ targetHost?: string,
692
+ targetPort?: number
693
+ ): void {
694
+ const connectionId = record.id;
695
+
696
+ // Determine target host and port if not provided
697
+ const finalTargetHost = targetHost || (domainConfig
698
+ ? this.domainConfigManager.getTargetIP(domainConfig)
699
+ : this.settings.defaults?.target?.host
700
+ ? this.settings.defaults.target.host
701
+ : this.settings.targetIP!);
702
+
703
+ // Determine target port - first try explicit port, then forwarding config, then fallback
704
+ const finalTargetPort = targetPort || (overridePort !== undefined
705
+ ? overridePort
706
+ : domainConfig
707
+ ? this.domainConfigManager.getTargetPort(domainConfig, this.settings.toPort)
708
+ : this.settings.defaults?.target?.port
709
+ ? this.settings.defaults.target.port
710
+ : this.settings.toPort);
711
+
712
+ // Setup connection options
713
+ const connectionOptions: plugins.net.NetConnectOpts = {
714
+ host: finalTargetHost,
715
+ port: finalTargetPort,
716
+ };
717
+
718
+ // Preserve source IP if configured
719
+ if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) {
720
+ connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
721
+ }
722
+
723
+ // Create a safe queue for incoming data
724
+ const dataQueue: Buffer[] = [];
725
+ let queueSize = 0;
726
+ let processingQueue = false;
727
+ let drainPending = false;
728
+ let pipingEstablished = false;
729
+
730
+ // Pause the incoming socket to prevent buffer overflows
731
+ socket.pause();
732
+
733
+ // Function to safely process the data queue without losing events
734
+ const processDataQueue = () => {
735
+ if (processingQueue || dataQueue.length === 0 || pipingEstablished) return;
736
+
737
+ processingQueue = true;
738
+
739
+ try {
740
+ // Process all queued chunks with the current active handler
741
+ while (dataQueue.length > 0) {
742
+ const chunk = dataQueue.shift()!;
743
+ queueSize -= chunk.length;
744
+
745
+ // Once piping is established, we shouldn't get here,
746
+ // but just in case, pass to the outgoing socket directly
747
+ if (pipingEstablished && record.outgoing) {
748
+ record.outgoing.write(chunk);
749
+ continue;
750
+ }
751
+
752
+ // Track bytes received
753
+ record.bytesReceived += chunk.length;
754
+
755
+ // Check for TLS handshake
756
+ if (!record.isTLS && this.tlsManager.isTlsHandshake(chunk)) {
757
+ record.isTLS = true;
758
+
759
+ if (this.settings.enableTlsDebugLogging) {
760
+ console.log(
761
+ `[${connectionId}] TLS handshake detected in tempDataHandler, ${chunk.length} bytes`
762
+ );
763
+ }
764
+ }
765
+
766
+ // Check if adding this chunk would exceed the buffer limit
767
+ const newSize = record.pendingDataSize + chunk.length;
768
+
769
+ if (this.settings.maxPendingDataSize && newSize > this.settings.maxPendingDataSize) {
770
+ console.log(
771
+ `[${connectionId}] Buffer limit exceeded for connection from ${record.remoteIP}: ${newSize} bytes > ${this.settings.maxPendingDataSize} bytes`
772
+ );
773
+ socket.end(); // Gracefully close the socket
774
+ this.connectionManager.initiateCleanupOnce(record, 'buffer_limit_exceeded');
775
+ return;
776
+ }
777
+
778
+ // Buffer the chunk and update the size counter
779
+ record.pendingData.push(Buffer.from(chunk));
780
+ record.pendingDataSize = newSize;
781
+ this.timeoutManager.updateActivity(record);
782
+ }
783
+ } finally {
784
+ processingQueue = false;
785
+
786
+ // If there's a pending drain and we've processed everything,
787
+ // signal we're ready for more data if we haven't established piping yet
788
+ if (drainPending && dataQueue.length === 0 && !pipingEstablished) {
789
+ drainPending = false;
790
+ socket.resume();
791
+ }
792
+ }
793
+ };
794
+
795
+ // Unified data handler that safely queues incoming data
796
+ const safeDataHandler = (chunk: Buffer) => {
797
+ // If piping is already established, just let the pipe handle it
798
+ if (pipingEstablished) return;
799
+
800
+ // Add to our queue for orderly processing
801
+ dataQueue.push(Buffer.from(chunk)); // Make a copy to be safe
802
+ queueSize += chunk.length;
803
+
804
+ // If queue is getting large, pause socket until we catch up
805
+ if (this.settings.maxPendingDataSize && queueSize > this.settings.maxPendingDataSize * 0.8) {
806
+ socket.pause();
807
+ drainPending = true;
808
+ }
809
+
810
+ // Process the queue
811
+ processDataQueue();
812
+ };
813
+
814
+ // Add our safe data handler
815
+ socket.on('data', safeDataHandler);
816
+
817
+ // Add initial chunk to pending data if present
818
+ if (initialChunk) {
819
+ record.bytesReceived += initialChunk.length;
820
+ record.pendingData.push(Buffer.from(initialChunk));
821
+ record.pendingDataSize = initialChunk.length;
822
+ }
823
+
824
+ // Create the target socket but don't set up piping immediately
825
+ const targetSocket = plugins.net.connect(connectionOptions);
826
+ record.outgoing = targetSocket;
827
+ record.outgoingStartTime = Date.now();
828
+
829
+ // Apply socket optimizations
830
+ targetSocket.setNoDelay(this.settings.noDelay);
831
+
832
+ // Apply keep-alive settings to the outgoing connection as well
833
+ if (this.settings.keepAlive) {
834
+ targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
835
+
836
+ // Apply enhanced TCP keep-alive options if enabled
837
+ if (this.settings.enableKeepAliveProbes) {
838
+ try {
839
+ if ('setKeepAliveProbes' in targetSocket) {
840
+ (targetSocket as any).setKeepAliveProbes(10);
841
+ }
842
+ if ('setKeepAliveInterval' in targetSocket) {
843
+ (targetSocket as any).setKeepAliveInterval(1000);
844
+ }
845
+ } catch (err) {
846
+ // Ignore errors - these are optional enhancements
847
+ if (this.settings.enableDetailedLogging) {
848
+ console.log(
849
+ `[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
850
+ );
851
+ }
852
+ }
853
+ }
854
+ }
855
+
856
+ // Setup specific error handler for connection phase
857
+ targetSocket.once('error', (err) => {
858
+ // This handler runs only once during the initial connection phase
859
+ const code = (err as any).code;
860
+ console.log(
861
+ `[${connectionId}] Connection setup error to ${finalTargetHost}:${connectionOptions.port}: ${err.message} (${code})`
862
+ );
863
+
864
+ // Resume the incoming socket to prevent it from hanging
865
+ socket.resume();
866
+
867
+ if (code === 'ECONNREFUSED') {
868
+ console.log(
869
+ `[${connectionId}] Target ${finalTargetHost}:${connectionOptions.port} refused connection`
870
+ );
871
+ } else if (code === 'ETIMEDOUT') {
872
+ console.log(
873
+ `[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} timed out`
874
+ );
875
+ } else if (code === 'ECONNRESET') {
876
+ console.log(
877
+ `[${connectionId}] Connection to ${finalTargetHost}:${connectionOptions.port} was reset`
878
+ );
879
+ } else if (code === 'EHOSTUNREACH') {
880
+ console.log(`[${connectionId}] Host ${finalTargetHost} is unreachable`);
881
+ }
882
+
883
+ // Clear any existing error handler after connection phase
884
+ targetSocket.removeAllListeners('error');
885
+
886
+ // Re-add the normal error handler for established connections
887
+ targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
888
+
889
+ if (record.outgoingTerminationReason === null) {
890
+ record.outgoingTerminationReason = 'connection_failed';
891
+ this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
892
+ }
893
+
894
+ // If we have a forwarding handler for this domain, let it handle the error
895
+ if (domainConfig) {
896
+ try {
897
+ const forwardingHandler = this.domainConfigManager.getForwardingHandler(domainConfig);
898
+ forwardingHandler.emit('connection_error', {
899
+ socket,
900
+ error: err,
901
+ connectionId
902
+ });
903
+ } catch (handlerErr) {
904
+ // If getting the handler fails, just log and continue with normal cleanup
905
+ console.log(`Error getting forwarding handler for error handling: ${handlerErr}`);
906
+ }
907
+ }
908
+
909
+ // Clean up the connection
910
+ this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
911
+ });
912
+
913
+ // Setup close handler
914
+ targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
915
+ socket.on('close', this.connectionManager.handleClose('incoming', record));
916
+
917
+ // Handle timeouts with keep-alive awareness
918
+ socket.on('timeout', () => {
919
+ // For keep-alive connections, just log a warning instead of closing
920
+ if (record.hasKeepAlive) {
921
+ console.log(
922
+ `[${connectionId}] Timeout event on incoming keep-alive connection from ${
923
+ record.remoteIP
924
+ } after ${plugins.prettyMs(
925
+ this.settings.socketTimeout || 3600000
926
+ )}. Connection preserved.`
927
+ );
928
+ return;
929
+ }
930
+
931
+ // For non-keep-alive connections, proceed with normal cleanup
932
+ console.log(
933
+ `[${connectionId}] Timeout on incoming side from ${
934
+ record.remoteIP
935
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
936
+ );
937
+ if (record.incomingTerminationReason === null) {
938
+ record.incomingTerminationReason = 'timeout';
939
+ this.connectionManager.incrementTerminationStat('incoming', 'timeout');
940
+ }
941
+ this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming');
942
+ });
943
+
944
+ targetSocket.on('timeout', () => {
945
+ // For keep-alive connections, just log a warning instead of closing
946
+ if (record.hasKeepAlive) {
947
+ console.log(
948
+ `[${connectionId}] Timeout event on outgoing keep-alive connection from ${
949
+ record.remoteIP
950
+ } after ${plugins.prettyMs(
951
+ this.settings.socketTimeout || 3600000
952
+ )}. Connection preserved.`
953
+ );
954
+ return;
955
+ }
956
+
957
+ // For non-keep-alive connections, proceed with normal cleanup
958
+ console.log(
959
+ `[${connectionId}] Timeout on outgoing side from ${
960
+ record.remoteIP
961
+ } after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
962
+ );
963
+ if (record.outgoingTerminationReason === null) {
964
+ record.outgoingTerminationReason = 'timeout';
965
+ this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
966
+ }
967
+ this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing');
968
+ });
969
+
970
+ // Apply socket timeouts
971
+ this.timeoutManager.applySocketTimeouts(record);
972
+
973
+ // Track outgoing data for bytes counting
974
+ targetSocket.on('data', (chunk: Buffer) => {
975
+ record.bytesSent += chunk.length;
976
+ this.timeoutManager.updateActivity(record);
977
+ });
978
+
979
+ // Wait for the outgoing connection to be ready before setting up piping
980
+ targetSocket.once('connect', () => {
981
+ // Clear the initial connection error handler
982
+ targetSocket.removeAllListeners('error');
983
+
984
+ // Add the normal error handler for established connections
985
+ targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
986
+
987
+ // Process any remaining data in the queue before switching to piping
988
+ processDataQueue();
989
+
990
+ // Set up piping immediately
991
+ pipingEstablished = true;
992
+
993
+ // Flush all pending data to target
994
+ if (record.pendingData.length > 0) {
995
+ const combinedData = Buffer.concat(record.pendingData);
996
+
997
+ if (this.settings.enableDetailedLogging) {
998
+ console.log(
999
+ `[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
1000
+ );
1001
+ }
1002
+
1003
+ // Write pending data immediately
1004
+ targetSocket.write(combinedData, (err) => {
1005
+ if (err) {
1006
+ console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
1007
+ return this.connectionManager.initiateCleanupOnce(record, 'write_error');
1008
+ }
1009
+ });
1010
+
1011
+ // Clear the buffer now that we've processed it
1012
+ record.pendingData = [];
1013
+ record.pendingDataSize = 0;
1014
+ }
1015
+
1016
+ // Setup piping in both directions without any delays
1017
+ socket.pipe(targetSocket);
1018
+ targetSocket.pipe(socket);
1019
+
1020
+ // Resume the socket to ensure data flows
1021
+ socket.resume();
1022
+
1023
+ // Process any data that might be queued in the interim
1024
+ if (dataQueue.length > 0) {
1025
+ // Write any remaining queued data directly to the target socket
1026
+ for (const chunk of dataQueue) {
1027
+ targetSocket.write(chunk);
1028
+ }
1029
+ // Clear the queue
1030
+ dataQueue.length = 0;
1031
+ queueSize = 0;
1032
+ }
1033
+
1034
+ if (this.settings.enableDetailedLogging) {
1035
+ console.log(
1036
+ `[${connectionId}] Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
1037
+ `${
1038
+ serverName
1039
+ ? ` (SNI: ${serverName})`
1040
+ : domainConfig
1041
+ ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1042
+ : ''
1043
+ }` +
1044
+ ` TLS: ${record.isTLS ? 'Yes' : 'No'}, Keep-Alive: ${
1045
+ record.hasKeepAlive ? 'Yes' : 'No'
1046
+ }`
1047
+ );
1048
+ } else {
1049
+ console.log(
1050
+ `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${connectionOptions.port}` +
1051
+ `${
1052
+ serverName
1053
+ ? ` (SNI: ${serverName})`
1054
+ : domainConfig
1055
+ ? ` (Port-based for domain: ${domainConfig.domains.join(', ')})`
1056
+ : ''
1057
+ }`
1058
+ );
1059
+ }
1060
+
1061
+ // Add the renegotiation handler for SNI validation
1062
+ if (serverName) {
1063
+ // Create connection info object for the existing connection
1064
+ const connInfo = {
1065
+ sourceIp: record.remoteIP,
1066
+ sourcePort: record.incoming.remotePort || 0,
1067
+ destIp: record.incoming.localAddress || '',
1068
+ destPort: record.incoming.localPort || 0,
1069
+ };
1070
+
1071
+ // Create a renegotiation handler function
1072
+ const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
1073
+ connectionId,
1074
+ serverName,
1075
+ connInfo,
1076
+ (connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
1077
+ );
1078
+
1079
+ // Store the handler in the connection record so we can remove it during cleanup
1080
+ record.renegotiationHandler = renegotiationHandler;
1081
+
1082
+ // Add the handler to the socket
1083
+ socket.on('data', renegotiationHandler);
1084
+
1085
+ if (this.settings.enableDetailedLogging) {
1086
+ console.log(
1087
+ `[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
1088
+ );
1089
+ if (this.settings.allowSessionTicket === false) {
1090
+ console.log(
1091
+ `[${connectionId}] Session ticket usage is disabled. Connection will be reset on reconnection attempts.`
1092
+ );
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ // Set connection timeout
1098
+ record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
1099
+ console.log(
1100
+ `[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
1101
+ );
1102
+ this.connectionManager.initiateCleanupOnce(record, reason);
1103
+ });
1104
+
1105
+ // Mark TLS handshake as complete for TLS connections
1106
+ if (record.isTLS) {
1107
+ record.tlsHandshakeComplete = true;
1108
+
1109
+ if (this.settings.enableTlsDebugLogging) {
1110
+ console.log(
1111
+ `[${connectionId}] TLS handshake complete for connection from ${record.remoteIP}`
1112
+ );
1113
+ }
1114
+ }
1115
+ });
1116
+ }
1117
+ }