@push.rocks/smartproxy 22.4.2 → 23.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 (101) hide show
  1. package/changelog.md +36 -0
  2. package/dist_rust/rustproxy +0 -0
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/index.d.ts +1 -6
  5. package/dist_ts/index.js +3 -11
  6. package/dist_ts/protocols/common/fragment-handler.js +5 -1
  7. package/dist_ts/proxies/index.d.ts +1 -6
  8. package/dist_ts/proxies/index.js +2 -8
  9. package/dist_ts/proxies/smart-proxy/index.d.ts +5 -10
  10. package/dist_ts/proxies/smart-proxy/index.js +7 -13
  11. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +5 -2
  12. package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
  13. package/dist_ts/proxies/smart-proxy/route-preprocessor.d.ts +37 -0
  14. package/dist_ts/proxies/smart-proxy/route-preprocessor.js +103 -0
  15. package/dist_ts/proxies/smart-proxy/rust-binary-locator.d.ts +23 -0
  16. package/dist_ts/proxies/smart-proxy/rust-binary-locator.js +104 -0
  17. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.d.ts +74 -0
  18. package/dist_ts/proxies/smart-proxy/rust-metrics-adapter.js +146 -0
  19. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.d.ts +49 -0
  20. package/dist_ts/proxies/smart-proxy/rust-proxy-bridge.js +259 -0
  21. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +39 -157
  22. package/dist_ts/proxies/smart-proxy/smart-proxy.js +224 -621
  23. package/dist_ts/proxies/smart-proxy/socket-handler-server.d.ts +45 -0
  24. package/dist_ts/proxies/smart-proxy/socket-handler-server.js +253 -0
  25. package/dist_ts/routing/index.d.ts +1 -1
  26. package/dist_ts/routing/index.js +3 -3
  27. package/dist_ts/routing/models/http-types.d.ts +119 -4
  28. package/dist_ts/routing/models/http-types.js +93 -5
  29. package/package.json +1 -1
  30. package/readme.md +444 -219
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/index.ts +4 -15
  33. package/ts/protocols/common/fragment-handler.ts +4 -0
  34. package/ts/proxies/index.ts +1 -12
  35. package/ts/proxies/smart-proxy/index.ts +6 -13
  36. package/ts/proxies/smart-proxy/models/interfaces.ts +6 -4
  37. package/ts/proxies/smart-proxy/models/route-types.ts +0 -2
  38. package/ts/proxies/smart-proxy/route-preprocessor.ts +122 -0
  39. package/ts/proxies/smart-proxy/rust-binary-locator.ts +112 -0
  40. package/ts/proxies/smart-proxy/rust-metrics-adapter.ts +161 -0
  41. package/ts/proxies/smart-proxy/rust-proxy-bridge.ts +310 -0
  42. package/ts/proxies/smart-proxy/smart-proxy.ts +282 -798
  43. package/ts/proxies/smart-proxy/socket-handler-server.ts +279 -0
  44. package/ts/routing/index.ts +2 -2
  45. package/ts/routing/models/http-types.ts +147 -4
  46. package/dist_ts/proxies/nftables-proxy/index.d.ts +0 -6
  47. package/dist_ts/proxies/nftables-proxy/index.js +0 -7
  48. package/dist_ts/proxies/nftables-proxy/models/errors.d.ts +0 -15
  49. package/dist_ts/proxies/nftables-proxy/models/errors.js +0 -28
  50. package/dist_ts/proxies/nftables-proxy/models/index.d.ts +0 -5
  51. package/dist_ts/proxies/nftables-proxy/models/index.js +0 -6
  52. package/dist_ts/proxies/nftables-proxy/models/interfaces.d.ts +0 -75
  53. package/dist_ts/proxies/nftables-proxy/models/interfaces.js +0 -5
  54. package/dist_ts/proxies/nftables-proxy/nftables-proxy.d.ts +0 -124
  55. package/dist_ts/proxies/nftables-proxy/nftables-proxy.js +0 -1374
  56. package/dist_ts/proxies/nftables-proxy/utils/index.d.ts +0 -9
  57. package/dist_ts/proxies/nftables-proxy/utils/index.js +0 -12
  58. package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.d.ts +0 -66
  59. package/dist_ts/proxies/nftables-proxy/utils/nft-command-executor.js +0 -131
  60. package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.d.ts +0 -39
  61. package/dist_ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.js +0 -112
  62. package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.d.ts +0 -59
  63. package/dist_ts/proxies/nftables-proxy/utils/nft-rule-validator.js +0 -130
  64. package/ts/proxies/http-proxy/connection-pool.ts +0 -228
  65. package/ts/proxies/http-proxy/context-creator.ts +0 -145
  66. package/ts/proxies/http-proxy/default-certificates.ts +0 -150
  67. package/ts/proxies/http-proxy/function-cache.ts +0 -279
  68. package/ts/proxies/http-proxy/handlers/index.ts +0 -5
  69. package/ts/proxies/http-proxy/http-proxy.ts +0 -669
  70. package/ts/proxies/http-proxy/http-request-handler.ts +0 -331
  71. package/ts/proxies/http-proxy/http2-request-handler.ts +0 -255
  72. package/ts/proxies/http-proxy/index.ts +0 -18
  73. package/ts/proxies/http-proxy/models/http-types.ts +0 -148
  74. package/ts/proxies/http-proxy/models/index.ts +0 -5
  75. package/ts/proxies/http-proxy/models/types.ts +0 -125
  76. package/ts/proxies/http-proxy/request-handler.ts +0 -878
  77. package/ts/proxies/http-proxy/security-manager.ts +0 -413
  78. package/ts/proxies/http-proxy/websocket-handler.ts +0 -581
  79. package/ts/proxies/nftables-proxy/index.ts +0 -6
  80. package/ts/proxies/nftables-proxy/models/errors.ts +0 -30
  81. package/ts/proxies/nftables-proxy/models/index.ts +0 -5
  82. package/ts/proxies/nftables-proxy/models/interfaces.ts +0 -94
  83. package/ts/proxies/nftables-proxy/nftables-proxy.ts +0 -1754
  84. package/ts/proxies/nftables-proxy/utils/index.ts +0 -38
  85. package/ts/proxies/nftables-proxy/utils/nft-command-executor.ts +0 -162
  86. package/ts/proxies/nftables-proxy/utils/nft-port-spec-normalizer.ts +0 -125
  87. package/ts/proxies/nftables-proxy/utils/nft-rule-validator.ts +0 -156
  88. package/ts/proxies/smart-proxy/acme-state-manager.ts +0 -112
  89. package/ts/proxies/smart-proxy/cert-store.ts +0 -92
  90. package/ts/proxies/smart-proxy/certificate-manager.ts +0 -895
  91. package/ts/proxies/smart-proxy/connection-manager.ts +0 -809
  92. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +0 -213
  93. package/ts/proxies/smart-proxy/metrics-collector.ts +0 -453
  94. package/ts/proxies/smart-proxy/nftables-manager.ts +0 -271
  95. package/ts/proxies/smart-proxy/port-manager.ts +0 -358
  96. package/ts/proxies/smart-proxy/route-connection-handler.ts +0 -1712
  97. package/ts/proxies/smart-proxy/route-orchestrator.ts +0 -297
  98. package/ts/proxies/smart-proxy/security-manager.ts +0 -269
  99. package/ts/proxies/smart-proxy/throughput-tracker.ts +0 -138
  100. package/ts/proxies/smart-proxy/timeout-manager.ts +0 -196
  101. package/ts/proxies/smart-proxy/tls-manager.ts +0 -171
@@ -1,1712 +0,0 @@
1
- import * as plugins from '../../plugins.js';
2
- import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
3
- import { logger } from '../../core/utils/logger.js';
4
- import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
5
- // Route checking functions have been removed
6
- import type { IRouteConfig, IRouteAction, IRouteTarget } from './models/route-types.js';
7
- import type { IRouteContext } from '../../core/models/route-context.js';
8
- import { cleanupSocket, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
9
- import { WrappedSocket } from '../../core/models/wrapped-socket.js';
10
- import { getUnderlyingSocket } from '../../core/models/socket-types.js';
11
- import { ProxyProtocolParser } from '../../core/utils/proxy-protocol.js';
12
- import type { SmartProxy } from './smart-proxy.js';
13
- import { ProtocolDetector } from '../../detection/index.js';
14
-
15
- /**
16
- * Handles new connection processing and setup logic with support for route-based configuration
17
- */
18
- export class RouteConnectionHandler {
19
- // Note: Route context caching was considered but not implemented
20
- // as route contexts are lightweight and should be created fresh
21
- // for each connection to ensure accurate context data
22
-
23
- // RxJS Subject for new connections
24
- public newConnectionSubject = new plugins.smartrx.rxjs.Subject<IConnectionRecord>();
25
-
26
- constructor(
27
- private smartProxy: SmartProxy
28
- ) {}
29
-
30
-
31
- /**
32
- * Create a route context object for port and host mapping functions
33
- */
34
- private createRouteContext(options: {
35
- connectionId: string;
36
- port: number;
37
- domain?: string;
38
- clientIp: string;
39
- serverIp: string;
40
- isTls: boolean;
41
- tlsVersion?: string;
42
- routeName?: string;
43
- routeId?: string;
44
- path?: string;
45
- query?: string;
46
- headers?: Record<string, string>;
47
- }): IRouteContext {
48
- return {
49
- // Connection information
50
- port: options.port,
51
- domain: options.domain,
52
- clientIp: options.clientIp,
53
- serverIp: options.serverIp,
54
- path: options.path,
55
- query: options.query,
56
- headers: options.headers,
57
-
58
- // TLS information
59
- isTls: options.isTls,
60
- tlsVersion: options.tlsVersion,
61
-
62
- // Route information
63
- routeName: options.routeName,
64
- routeId: options.routeId,
65
-
66
- // Additional properties
67
- timestamp: Date.now(),
68
- connectionId: options.connectionId,
69
- };
70
- }
71
-
72
- /**
73
- * Determines if SNI is required for routing decisions on this port.
74
- *
75
- * SNI is REQUIRED when:
76
- * - Multiple routes exist on this port (need SNI to pick correct route)
77
- * - Route has dynamic target function (needs ctx.domain)
78
- * - Route has specific domain restriction (strict validation)
79
- *
80
- * SNI is NOT required when:
81
- * - TLS termination mode (HttpProxy handles session resumption)
82
- * - Single route with static target and no domain restriction (or wildcard)
83
- */
84
- private calculateSniRequirement(port: number): boolean {
85
- const routesOnPort = this.smartProxy.routeManager.getRoutesForPort(port);
86
-
87
- // No routes = no SNI requirement (will fail routing anyway)
88
- if (routesOnPort.length === 0) return false;
89
-
90
- // Check if any route terminates TLS - if so, SNI not required
91
- // (HttpProxy handles session resumption internally)
92
- const hasTermination = routesOnPort.some(route =>
93
- route.action.tls?.mode === 'terminate' ||
94
- route.action.tls?.mode === 'terminate-and-reencrypt'
95
- );
96
- if (hasTermination) return false;
97
-
98
- // Multiple routes = need SNI to pick the correct route
99
- if (routesOnPort.length > 1) return true;
100
-
101
- // Single route - check if it needs SNI for validation or routing
102
- const route = routesOnPort[0];
103
-
104
- // Dynamic host selection requires SNI (function receives ctx.domain)
105
- const hasDynamicTarget = route.action.targets?.some(t => typeof t.host === 'function');
106
- if (hasDynamicTarget) return true;
107
-
108
- // Specific domain restriction requires SNI for strict validation
109
- const hasSpecificDomain = route.match.domains && !this.isWildcardOnly(route.match.domains);
110
- if (hasSpecificDomain) return true;
111
-
112
- // Single route, static target(s), no domain restriction = SNI not required
113
- return false;
114
- }
115
-
116
- /**
117
- * Check if domains config is wildcard-only (matches everything)
118
- */
119
- private isWildcardOnly(domains: string | string[]): boolean {
120
- const domainList = Array.isArray(domains) ? domains : [domains];
121
- return domainList.length === 1 && domainList[0] === '*';
122
- }
123
-
124
- /**
125
- * Handle a new incoming connection
126
- */
127
- public handleConnection(socket: plugins.net.Socket): void {
128
- const remoteIP = socket.remoteAddress || '';
129
- const localPort = socket.localPort || 0;
130
-
131
- // Always wrap the socket to prepare for potential PROXY protocol
132
- const wrappedSocket = new WrappedSocket(socket);
133
-
134
- // If this is from a trusted proxy, log it
135
- if (this.smartProxy.settings.proxyIPs?.includes(remoteIP)) {
136
- logger.log('debug', `Connection from trusted proxy ${remoteIP}, PROXY protocol parsing will be enabled`, {
137
- remoteIP,
138
- component: 'route-handler'
139
- });
140
- }
141
-
142
- // Generate connection ID first for atomic IP validation and tracking
143
- const connectionId = this.smartProxy.connectionManager.generateConnectionId();
144
- const clientIP = wrappedSocket.remoteAddress || '';
145
-
146
- // Atomically validate IP and track the connection to prevent race conditions
147
- // This ensures concurrent connections from the same IP are properly limited
148
- const ipValidation = this.smartProxy.securityManager.validateAndTrackIP(clientIP, connectionId);
149
- if (!ipValidation.allowed) {
150
- connectionLogDeduplicator.log(
151
- 'ip-rejected',
152
- 'warn',
153
- `Connection rejected from ${clientIP}`,
154
- { remoteIP: clientIP, reason: ipValidation.reason, component: 'route-handler' },
155
- clientIP
156
- );
157
- cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
158
- return;
159
- }
160
-
161
- // Create a new connection record with the wrapped socket
162
- // Skip IP tracking since we already did it atomically above
163
- const record = this.smartProxy.connectionManager.createConnection(wrappedSocket, {
164
- connectionId,
165
- skipIpTracking: true
166
- });
167
- if (!record) {
168
- // Connection was rejected due to global limit - clean up the IP tracking we did
169
- this.smartProxy.securityManager.removeConnectionByIP(clientIP, connectionId);
170
- return;
171
- }
172
-
173
- // Emit new connection event
174
- this.newConnectionSubject.next(record);
175
- // Note: connectionId was already generated above for atomic IP tracking
176
-
177
- // Apply socket optimizations (apply to underlying socket)
178
- const underlyingSocket = wrappedSocket.socket;
179
- underlyingSocket.setNoDelay(this.smartProxy.settings.noDelay);
180
-
181
- // Apply keep-alive settings if enabled
182
- if (this.smartProxy.settings.keepAlive) {
183
- underlyingSocket.setKeepAlive(true, this.smartProxy.settings.keepAliveInitialDelay);
184
- record.hasKeepAlive = true;
185
-
186
- // Apply enhanced TCP keep-alive options if enabled
187
- if (this.smartProxy.settings.enableKeepAliveProbes) {
188
- try {
189
- // These are platform-specific and may not be available
190
- if ('setKeepAliveProbes' in underlyingSocket) {
191
- (underlyingSocket as any).setKeepAliveProbes(10);
192
- }
193
- if ('setKeepAliveInterval' in underlyingSocket) {
194
- (underlyingSocket as any).setKeepAliveInterval(1000);
195
- }
196
- } catch (err) {
197
- // Ignore errors - these are optional enhancements
198
- if (this.smartProxy.settings.enableDetailedLogging) {
199
- logger.log('warn', `Enhanced TCP keep-alive settings not supported`, { connectionId, error: err, component: 'route-handler' });
200
- }
201
- }
202
- }
203
- }
204
-
205
- if (this.smartProxy.settings.enableDetailedLogging) {
206
- logger.log('info',
207
- `New connection from ${remoteIP} on port ${localPort}. ` +
208
- `Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
209
- `Active connections: ${this.smartProxy.connectionManager.getConnectionCount()}`,
210
- {
211
- connectionId,
212
- remoteIP,
213
- localPort,
214
- keepAlive: record.hasKeepAlive ? 'Enabled' : 'Disabled',
215
- activeConnections: this.smartProxy.connectionManager.getConnectionCount(),
216
- component: 'route-handler'
217
- }
218
- );
219
- } else {
220
- logger.log('info',
221
- `New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.smartProxy.connectionManager.getConnectionCount()}`,
222
- {
223
- remoteIP,
224
- localPort,
225
- activeConnections: this.smartProxy.connectionManager.getConnectionCount(),
226
- component: 'route-handler'
227
- }
228
- );
229
- }
230
-
231
- // Handle the connection - wait for initial data to determine if it's TLS
232
- this.handleInitialData(wrappedSocket, record);
233
- }
234
-
235
- /**
236
- * Handle initial data from a connection to determine routing
237
- */
238
- private handleInitialData(socket: plugins.net.Socket | WrappedSocket, record: IConnectionRecord): void {
239
- const connectionId = record.id;
240
- const localPort = record.localPort;
241
- let initialDataReceived = false;
242
-
243
- // Check if any routes on this port require TLS handling
244
- const allRoutes = this.smartProxy.routeManager.getRoutes();
245
- const needsTlsHandling = allRoutes.some(route => {
246
- // Check if route matches this port
247
- const matchesPort = this.smartProxy.routeManager.getRoutesForPort(localPort).includes(route);
248
-
249
- return matchesPort &&
250
- route.action.type === 'forward' &&
251
- route.action.tls &&
252
- (route.action.tls.mode === 'terminate' ||
253
- route.action.tls.mode === 'passthrough');
254
- });
255
-
256
- // Smart SNI requirement calculation
257
- // Determines if we need SNI for routing decisions on this port
258
- const needsSniForRouting = this.calculateSniRequirement(localPort);
259
- const allowSessionTicket = !needsSniForRouting;
260
-
261
- // If no routes require TLS handling and it's not port 443, route immediately
262
- if (!needsTlsHandling && localPort !== 443) {
263
- // Extract underlying socket for socket-utils functions
264
- const underlyingSocket = getUnderlyingSocket(socket);
265
- // Set up proper socket handlers for immediate routing
266
- setupSocketHandlers(
267
- underlyingSocket,
268
- (reason) => {
269
- // Always cleanup when incoming socket closes
270
- // This prevents connection accumulation in proxy chains
271
- logger.log('debug', `Connection ${connectionId} closed during immediate routing: ${reason}`, {
272
- connectionId,
273
- remoteIP: record.remoteIP,
274
- reason,
275
- hasOutgoing: !!record.outgoing,
276
- outgoingState: record.outgoing?.readyState,
277
- component: 'route-handler'
278
- });
279
-
280
- // If there's a pending or established outgoing connection, destroy it
281
- if (record.outgoing && !record.outgoing.destroyed) {
282
- logger.log('debug', `Destroying outgoing connection for ${connectionId}`, {
283
- connectionId,
284
- outgoingState: record.outgoing.readyState,
285
- component: 'route-handler'
286
- });
287
- record.outgoing.destroy();
288
- }
289
-
290
- // Always cleanup the connection record
291
- this.smartProxy.connectionManager.cleanupConnection(record, reason);
292
- },
293
- undefined, // Use default timeout handler
294
- 'immediate-route-client'
295
- );
296
-
297
- // Route immediately for non-TLS connections
298
- this.routeConnection(socket, record, '', undefined);
299
- return;
300
- }
301
-
302
- // Otherwise, wait for initial data to check if it's TLS
303
- // Set an initial timeout for handshake data
304
- let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
305
- if (!initialDataReceived) {
306
- logger.log('warn', `No initial data received from ${record.remoteIP} after ${this.smartProxy.settings.initialDataTimeout}ms for connection ${connectionId}`, {
307
- connectionId,
308
- timeout: this.smartProxy.settings.initialDataTimeout,
309
- remoteIP: record.remoteIP,
310
- component: 'route-handler'
311
- });
312
-
313
- // Add a grace period
314
- setTimeout(() => {
315
- if (!initialDataReceived) {
316
- logger.log('warn', `Final initial data timeout after grace period for connection ${connectionId}`, {
317
- connectionId,
318
- component: 'route-handler'
319
- });
320
- if (record.incomingTerminationReason === null) {
321
- record.incomingTerminationReason = 'initial_timeout';
322
- this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
323
- }
324
- socket.end();
325
- this.smartProxy.connectionManager.cleanupConnection(record, 'initial_timeout');
326
- }
327
- }, 30000);
328
- }
329
- }, this.smartProxy.settings.initialDataTimeout!);
330
-
331
- // Make sure timeout doesn't keep the process alive
332
- if (initialTimeout.unref) {
333
- initialTimeout.unref();
334
- }
335
-
336
- // Set up error handler
337
- socket.on('error', this.smartProxy.connectionManager.handleError('incoming', record));
338
-
339
- // Add close/end handlers to catch immediate disconnections
340
- socket.once('close', () => {
341
- if (!initialDataReceived) {
342
- logger.log('warn', `Connection ${connectionId} closed before sending initial data`, {
343
- connectionId,
344
- remoteIP: record.remoteIP,
345
- component: 'route-handler'
346
- });
347
- if (initialTimeout) {
348
- clearTimeout(initialTimeout);
349
- initialTimeout = null;
350
- }
351
- this.smartProxy.connectionManager.cleanupConnection(record, 'closed_before_data');
352
- }
353
- });
354
-
355
- socket.once('end', () => {
356
- if (!initialDataReceived) {
357
- logger.log('debug', `Connection ${connectionId} ended before sending initial data`, {
358
- connectionId,
359
- remoteIP: record.remoteIP,
360
- component: 'route-handler'
361
- });
362
- if (initialTimeout) {
363
- clearTimeout(initialTimeout);
364
- initialTimeout = null;
365
- }
366
- // Don't cleanup on 'end' - wait for 'close'
367
- }
368
- });
369
-
370
- // Handler for processing initial data (after potential PROXY protocol)
371
- const processInitialData = async (chunk: Buffer) => {
372
- // Create connection context for protocol detection
373
- const context = ProtocolDetector.createConnectionContext({
374
- sourceIp: record.remoteIP,
375
- sourcePort: socket.remotePort || 0,
376
- destIp: socket.localAddress || '',
377
- destPort: socket.localPort || 0,
378
- socketId: record.id
379
- });
380
-
381
- const detectionResult = await ProtocolDetector.detectWithContext(
382
- chunk,
383
- context,
384
- { extractFullHeaders: false } // Only extract essential info for routing
385
- );
386
-
387
- // Block non-TLS connections on port 443
388
- if (localPort === 443 && detectionResult.protocol !== 'tls') {
389
- logger.log('warn', `Non-TLS connection ${record.id} detected on port 443. Terminating connection - only TLS traffic is allowed on standard HTTPS port.`, {
390
- connectionId: record.id,
391
- detectedProtocol: detectionResult.protocol,
392
- message: 'Terminating connection - only TLS traffic is allowed on standard HTTPS port.',
393
- component: 'route-handler'
394
- });
395
- if (record.incomingTerminationReason === null) {
396
- record.incomingTerminationReason = 'non_tls_blocked';
397
- this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
398
- }
399
- socket.end();
400
- this.smartProxy.connectionManager.cleanupConnection(record, 'non_tls_blocked');
401
- return;
402
- }
403
-
404
- // Extract domain and protocol info
405
- let serverName = '';
406
- if (detectionResult.protocol === 'tls') {
407
- record.isTLS = true;
408
- serverName = detectionResult.connectionInfo.domain || '';
409
-
410
- // Lock the connection to the negotiated SNI
411
- record.lockedDomain = serverName;
412
-
413
- // Check if we should reject connections without SNI
414
- if (!serverName && allowSessionTicket === false) {
415
- logger.log('warn', `No SNI detected in TLS ClientHello for connection ${record.id}; sending TLS alert`, {
416
- connectionId: record.id,
417
- component: 'route-handler'
418
- });
419
- if (record.incomingTerminationReason === null) {
420
- record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
421
- this.smartProxy.connectionManager.incrementTerminationStat(
422
- 'incoming',
423
- 'session_ticket_blocked_no_sni'
424
- );
425
- }
426
- const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
427
- try {
428
- // Count the alert bytes being sent
429
- record.bytesSent += alert.length;
430
- if (this.smartProxy.metricsCollector) {
431
- this.smartProxy.metricsCollector.recordBytes(record.id, 0, alert.length);
432
- }
433
-
434
- socket.cork();
435
- socket.write(alert);
436
- socket.uncork();
437
- socket.end();
438
- } catch {
439
- socket.end();
440
- }
441
- this.smartProxy.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
442
- return;
443
- }
444
-
445
- if (this.smartProxy.settings.enableDetailedLogging) {
446
- logger.log('info', `TLS connection with SNI`, {
447
- connectionId: record.id,
448
- serverName: serverName || '(empty)',
449
- component: 'route-handler'
450
- });
451
- }
452
- } else if (detectionResult.protocol === 'http') {
453
- // For HTTP, extract domain from Host header
454
- serverName = detectionResult.connectionInfo.domain || '';
455
-
456
- // Store HTTP-specific info for later use
457
- record.httpInfo = {
458
- method: detectionResult.connectionInfo.method,
459
- path: detectionResult.connectionInfo.path,
460
- headers: detectionResult.connectionInfo.headers
461
- };
462
-
463
- if (this.smartProxy.settings.enableDetailedLogging) {
464
- logger.log('info', `HTTP connection detected`, {
465
- connectionId: record.id,
466
- domain: serverName || '(no host header)',
467
- method: detectionResult.connectionInfo.method,
468
- path: detectionResult.connectionInfo.path,
469
- component: 'route-handler'
470
- });
471
- }
472
- }
473
-
474
- // Find the appropriate route for this connection
475
- this.routeConnection(socket, record, serverName, chunk, detectionResult);
476
- };
477
-
478
- // First data handler to capture initial TLS handshake or PROXY protocol
479
- socket.once('data', async (chunk: Buffer) => {
480
- // Clear the initial timeout since we've received data
481
- if (initialTimeout) {
482
- clearTimeout(initialTimeout);
483
- initialTimeout = null;
484
- }
485
-
486
- initialDataReceived = true;
487
- record.hasReceivedInitialData = true;
488
-
489
- // Check if this is from a trusted proxy and might have PROXY protocol
490
- if (this.smartProxy.settings.proxyIPs?.includes(socket.remoteAddress || '') && this.smartProxy.settings.acceptProxyProtocol !== false) {
491
- // Check if this starts with PROXY protocol
492
- if (chunk.toString('ascii', 0, Math.min(6, chunk.length)).startsWith('PROXY ')) {
493
- try {
494
- const parseResult = ProxyProtocolParser.parse(chunk);
495
-
496
- if (parseResult.proxyInfo) {
497
- // Update the wrapped socket with real client info (if it's a WrappedSocket)
498
- if (socket instanceof WrappedSocket) {
499
- socket.setProxyInfo(parseResult.proxyInfo.sourceIP, parseResult.proxyInfo.sourcePort);
500
- }
501
-
502
- // Update connection record with real client info
503
- record.remoteIP = parseResult.proxyInfo.sourceIP;
504
- record.remotePort = parseResult.proxyInfo.sourcePort;
505
-
506
- logger.log('info', `PROXY protocol parsed successfully`, {
507
- connectionId,
508
- realClientIP: parseResult.proxyInfo.sourceIP,
509
- realClientPort: parseResult.proxyInfo.sourcePort,
510
- proxyIP: socket.remoteAddress,
511
- component: 'route-handler'
512
- });
513
-
514
- // Process remaining data if any
515
- if (parseResult.remainingData.length > 0) {
516
- processInitialData(parseResult.remainingData);
517
- } else {
518
- // Wait for more data
519
- socket.once('data', processInitialData);
520
- }
521
- return;
522
- }
523
- } catch (error) {
524
- logger.log('error', `Failed to parse PROXY protocol from trusted proxy`, {
525
- connectionId,
526
- error: error.message,
527
- proxyIP: socket.remoteAddress,
528
- component: 'route-handler'
529
- });
530
- // Continue processing as normal data
531
- }
532
- }
533
- }
534
-
535
- // Process as normal data (no PROXY protocol)
536
- processInitialData(chunk);
537
- });
538
- }
539
-
540
- /**
541
- * Route the connection based on match criteria
542
- */
543
- private routeConnection(
544
- socket: plugins.net.Socket | WrappedSocket,
545
- record: IConnectionRecord,
546
- serverName: string,
547
- initialChunk?: Buffer,
548
- detectionResult?: any // Using any temporarily to avoid circular dependency issues
549
- ): void {
550
- const connectionId = record.id;
551
- const localPort = record.localPort;
552
- const remoteIP = record.remoteIP;
553
-
554
- // Check if this is an HTTP proxy port
555
- const isHttpProxyPort = this.smartProxy.settings.useHttpProxy?.includes(localPort);
556
-
557
- // For HTTP proxy ports without TLS, skip domain check since domain info comes from HTTP headers
558
- const skipDomainCheck = isHttpProxyPort && !record.isTLS;
559
-
560
- // Create route context for matching
561
- const routeContext: IRouteContext = {
562
- port: localPort,
563
- domain: skipDomainCheck ? undefined : serverName, // Skip domain if HTTP proxy without TLS
564
- clientIp: remoteIP,
565
- serverIp: socket.localAddress || '',
566
- path: undefined, // We don't have path info at this point
567
- isTls: record.isTLS,
568
- tlsVersion: undefined, // We don't extract TLS version yet
569
- timestamp: Date.now(),
570
- connectionId: record.id
571
- };
572
-
573
- // Find matching route
574
- const routeMatch = this.smartProxy.routeManager.findMatchingRoute(routeContext);
575
-
576
- if (!routeMatch) {
577
- logger.log('warn', `No route found for ${serverName || 'connection'} on port ${localPort} (connection: ${connectionId})`, {
578
- connectionId,
579
- serverName: serverName || 'connection',
580
- localPort,
581
- component: 'route-handler'
582
- });
583
-
584
- // No matching route, use default/fallback handling
585
- logger.log('info', `Using default route handling for connection ${connectionId}`, {
586
- connectionId,
587
- component: 'route-handler'
588
- });
589
-
590
- // Check default security settings
591
- const defaultSecuritySettings = this.smartProxy.settings.defaults?.security;
592
- if (defaultSecuritySettings) {
593
- if (defaultSecuritySettings.ipAllowList && defaultSecuritySettings.ipAllowList.length > 0) {
594
- const isAllowed = this.smartProxy.securityManager.isIPAuthorized(
595
- remoteIP,
596
- defaultSecuritySettings.ipAllowList,
597
- defaultSecuritySettings.ipBlockList || []
598
- );
599
-
600
- if (!isAllowed) {
601
- logger.log('warn', `IP ${remoteIP} not in default allowed list for connection ${connectionId}`, {
602
- connectionId,
603
- remoteIP,
604
- component: 'route-handler'
605
- });
606
- socket.end();
607
- this.smartProxy.connectionManager.cleanupConnection(record, 'ip_blocked');
608
- return;
609
- }
610
- }
611
- }
612
-
613
- // Setup direct connection with default settings
614
- if (this.smartProxy.settings.defaults?.target) {
615
- // Use defaults from configuration
616
- const targetHost = this.smartProxy.settings.defaults.target.host;
617
- const targetPort = this.smartProxy.settings.defaults.target.port;
618
-
619
- return this.setupDirectConnection(
620
- socket,
621
- record,
622
- serverName,
623
- initialChunk,
624
- undefined,
625
- targetHost,
626
- targetPort
627
- );
628
- } else {
629
- // No default target available, terminate the connection
630
- logger.log('warn', `No default target configured for connection ${connectionId}. Closing connection`, {
631
- connectionId,
632
- component: 'route-handler'
633
- });
634
- socket.end();
635
- this.smartProxy.connectionManager.cleanupConnection(record, 'no_default_target');
636
- return;
637
- }
638
- }
639
-
640
- // A matching route was found
641
- const route = routeMatch.route;
642
-
643
- if (this.smartProxy.settings.enableDetailedLogging) {
644
- logger.log('info', `Route matched`, {
645
- connectionId,
646
- routeName: route.name || 'unnamed',
647
- serverName: serverName || 'connection',
648
- localPort,
649
- component: 'route-handler'
650
- });
651
- }
652
-
653
- // Apply route-specific security checks
654
- if (route.security) {
655
- // Check IP allow/block lists
656
- if (route.security.ipAllowList || route.security.ipBlockList) {
657
- const isIPAllowed = this.smartProxy.securityManager.isIPAuthorized(
658
- remoteIP,
659
- route.security.ipAllowList || [],
660
- route.security.ipBlockList || []
661
- );
662
-
663
- if (!isIPAllowed) {
664
- // Deduplicated logging for route IP blocks
665
- connectionLogDeduplicator.log(
666
- 'ip-rejected',
667
- 'warn',
668
- `IP blocked by route security`,
669
- {
670
- connectionId,
671
- remoteIP,
672
- routeName: route.name || 'unnamed',
673
- reason: 'route-ip-blocked',
674
- component: 'route-handler'
675
- },
676
- remoteIP
677
- );
678
- socket.end();
679
- this.smartProxy.connectionManager.cleanupConnection(record, 'route_ip_blocked');
680
- return;
681
- }
682
- }
683
-
684
- // Check max connections per route
685
- if (route.security.maxConnections !== undefined) {
686
- const routeId = route.id || route.name || 'unnamed';
687
- const currentConnections = this.smartProxy.connectionManager.getConnectionCountByRoute(routeId);
688
-
689
- if (currentConnections >= route.security.maxConnections) {
690
- // Deduplicated logging for route connection limits
691
- connectionLogDeduplicator.log(
692
- 'connection-rejected',
693
- 'warn',
694
- `Route connection limit reached`,
695
- {
696
- connectionId,
697
- routeName: route.name,
698
- currentConnections,
699
- maxConnections: route.security.maxConnections,
700
- reason: 'route-limit',
701
- component: 'route-handler'
702
- },
703
- `route-limit-${route.name}`
704
- );
705
- socket.end();
706
- this.smartProxy.connectionManager.cleanupConnection(record, 'route_connection_limit');
707
- return;
708
- }
709
- }
710
-
711
- // Check authentication requirements
712
- if (route.security.authentication || route.security.basicAuth || route.security.jwtAuth) {
713
- // Authentication checks would typically happen at the HTTP layer
714
- // For non-HTTP connections or passthrough, we can't enforce authentication
715
- if (route.action.type === 'forward' && route.action.tls?.mode !== 'terminate') {
716
- logger.log('warn', `Route ${route.name} has authentication configured but it cannot be enforced for non-terminated connections`, {
717
- connectionId,
718
- routeName: route.name,
719
- tlsMode: route.action.tls?.mode || 'none',
720
- component: 'route-handler'
721
- });
722
- }
723
- }
724
- }
725
-
726
- // Handle the route based on its action type
727
- switch (route.action.type) {
728
- case 'forward':
729
- return this.handleForwardAction(socket, record, route, initialChunk, detectionResult);
730
-
731
- case 'socket-handler':
732
- logger.log('info', `Handling socket-handler action for route ${route.name}`, {
733
- connectionId,
734
- routeName: route.name,
735
- component: 'route-handler'
736
- });
737
- this.handleSocketHandlerAction(socket, record, route, initialChunk);
738
- return;
739
-
740
- default:
741
- logger.log('error', `Unknown action type '${(route.action as any).type}' for connection ${connectionId}`, {
742
- connectionId,
743
- actionType: (route.action as any).type,
744
- component: 'route-handler'
745
- });
746
- socket.end();
747
- this.smartProxy.connectionManager.cleanupConnection(record, 'unknown_action');
748
- }
749
- }
750
-
751
- /**
752
- * Select the appropriate target from the targets array based on sub-matching criteria
753
- */
754
- private selectTarget(
755
- targets: IRouteTarget[],
756
- context: {
757
- port: number;
758
- path?: string;
759
- headers?: Record<string, string>;
760
- method?: string;
761
- }
762
- ): IRouteTarget | null {
763
- // Sort targets by priority (higher first)
764
- const sortedTargets = [...targets].sort((a, b) => (b.priority || 0) - (a.priority || 0));
765
-
766
- // Find the first matching target
767
- for (const target of sortedTargets) {
768
- if (!target.match) {
769
- // No match criteria means this is a default/fallback target
770
- return target;
771
- }
772
-
773
- // Check port match
774
- if (target.match.ports && !target.match.ports.includes(context.port)) {
775
- continue;
776
- }
777
-
778
- // Check path match (supports wildcards)
779
- if (target.match.path && context.path) {
780
- const pathPattern = target.match.path.replace(/\*/g, '.*');
781
- const pathRegex = new RegExp(`^${pathPattern}$`);
782
- if (!pathRegex.test(context.path)) {
783
- continue;
784
- }
785
- }
786
-
787
- // Check method match
788
- if (target.match.method && context.method && !target.match.method.includes(context.method)) {
789
- continue;
790
- }
791
-
792
- // Check headers match
793
- if (target.match.headers && context.headers) {
794
- let headersMatch = true;
795
- for (const [key, pattern] of Object.entries(target.match.headers)) {
796
- const headerValue = context.headers[key.toLowerCase()];
797
- if (!headerValue) {
798
- headersMatch = false;
799
- break;
800
- }
801
-
802
- if (pattern instanceof RegExp) {
803
- if (!pattern.test(headerValue)) {
804
- headersMatch = false;
805
- break;
806
- }
807
- } else if (headerValue !== pattern) {
808
- headersMatch = false;
809
- break;
810
- }
811
- }
812
- if (!headersMatch) {
813
- continue;
814
- }
815
- }
816
-
817
- // All criteria matched
818
- return target;
819
- }
820
-
821
- // No matching target found, return the first target without match criteria (default)
822
- return sortedTargets.find(t => !t.match) || null;
823
- }
824
-
825
- /**
826
- * Handle a forward action for a route
827
- */
828
- private handleForwardAction(
829
- socket: plugins.net.Socket | WrappedSocket,
830
- record: IConnectionRecord,
831
- route: IRouteConfig,
832
- initialChunk?: Buffer,
833
- detectionResult?: any // Using any temporarily to avoid circular dependency issues
834
- ): void {
835
- const connectionId = record.id;
836
- const action = route.action as IRouteAction;
837
-
838
- // Store the route config in the connection record for metrics and other uses
839
- record.routeConfig = route;
840
- record.routeId = route.id || route.name || 'unnamed';
841
-
842
- // Track connection by route
843
- this.smartProxy.connectionManager.trackConnectionByRoute(record.routeId, record.id);
844
-
845
- // Check if this route uses NFTables for forwarding
846
- if (action.forwardingEngine === 'nftables') {
847
- // NFTables handles packet forwarding at the kernel level
848
- // The application should NOT interfere with these connections
849
-
850
- // Log the connection for monitoring purposes
851
- if (this.smartProxy.settings.enableDetailedLogging) {
852
- logger.log('info', `NFTables forwarding (kernel-level)`, {
853
- connectionId: record.id,
854
- source: `${record.remoteIP}:${socket.remotePort}`,
855
- destination: `${socket.localAddress}:${record.localPort}`,
856
- routeName: route.name || 'unnamed',
857
- domain: record.lockedDomain || 'n/a',
858
- component: 'route-handler'
859
- });
860
- } else {
861
- logger.log('info', `NFTables forwarding`, {
862
- connectionId: record.id,
863
- remoteIP: record.remoteIP,
864
- localPort: record.localPort,
865
- routeName: route.name || 'unnamed',
866
- component: 'route-handler'
867
- });
868
- }
869
-
870
- // Additional NFTables-specific logging if configured
871
- if (action.nftables) {
872
- const nftConfig = action.nftables;
873
- if (this.smartProxy.settings.enableDetailedLogging) {
874
- logger.log('info', `NFTables config`, {
875
- connectionId: record.id,
876
- protocol: nftConfig.protocol || 'tcp',
877
- preserveSourceIP: nftConfig.preserveSourceIP || false,
878
- priority: nftConfig.priority || 'default',
879
- maxRate: nftConfig.maxRate || 'unlimited',
880
- component: 'route-handler'
881
- });
882
- }
883
- }
884
-
885
- // For NFTables routes, we should still track the connection but not interfere
886
- // Mark the connection as using network proxy so it's cleaned up properly
887
- record.usingNetworkProxy = true;
888
-
889
- // We don't close the socket - just let it remain open
890
- // The kernel-level NFTables rules will handle the actual forwarding
891
-
892
- // Set up cleanup when the socket eventually closes
893
- socket.once('close', () => {
894
- this.smartProxy.connectionManager.cleanupConnection(record, 'nftables_closed');
895
- });
896
-
897
- return;
898
- }
899
-
900
- // Select the appropriate target from the targets array
901
- if (!action.targets || action.targets.length === 0) {
902
- logger.log('error', `Forward action missing targets configuration for connection ${connectionId}`, {
903
- connectionId,
904
- component: 'route-handler'
905
- });
906
- socket.end();
907
- this.smartProxy.connectionManager.cleanupConnection(record, 'missing_targets');
908
- return;
909
- }
910
-
911
- // Create context for target selection
912
- const targetSelectionContext = {
913
- port: record.localPort,
914
- path: record.httpInfo?.path,
915
- headers: record.httpInfo?.headers,
916
- method: record.httpInfo?.method
917
- };
918
-
919
- const selectedTarget = this.selectTarget(action.targets, targetSelectionContext);
920
- if (!selectedTarget) {
921
- logger.log('error', `No matching target found for connection ${connectionId}`, {
922
- connectionId,
923
- port: targetSelectionContext.port,
924
- component: 'route-handler'
925
- });
926
- socket.end();
927
- this.smartProxy.connectionManager.cleanupConnection(record, 'no_matching_target');
928
- return;
929
- }
930
-
931
- // Create the routing context for this connection
932
- const routeContext = this.createRouteContext({
933
- connectionId: record.id,
934
- port: record.localPort,
935
- domain: record.lockedDomain,
936
- clientIp: record.remoteIP,
937
- serverIp: socket.localAddress || '',
938
- isTls: record.isTLS || false,
939
- tlsVersion: record.tlsVersion,
940
- routeName: route.name,
941
- routeId: route.id,
942
- });
943
-
944
- // Note: Route contexts are not cached to ensure fresh data for each connection
945
-
946
- // Determine host using function or static value
947
- let targetHost: string | string[];
948
- if (typeof selectedTarget.host === 'function') {
949
- try {
950
- targetHost = selectedTarget.host(routeContext);
951
- if (this.smartProxy.settings.enableDetailedLogging) {
952
- logger.log('info', `Dynamic host resolved to ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost} for connection ${connectionId}`, {
953
- connectionId,
954
- targetHost: Array.isArray(targetHost) ? targetHost.join(', ') : targetHost,
955
- component: 'route-handler'
956
- });
957
- }
958
- } catch (err) {
959
- logger.log('error', `Error in host mapping function for connection ${connectionId}: ${err}`, {
960
- connectionId,
961
- error: err,
962
- component: 'route-handler'
963
- });
964
- socket.end();
965
- this.smartProxy.connectionManager.cleanupConnection(record, 'host_mapping_error');
966
- return;
967
- }
968
- } else {
969
- targetHost = selectedTarget.host;
970
- }
971
-
972
- // If an array of hosts, select one randomly for load balancing
973
- const selectedHost = Array.isArray(targetHost)
974
- ? targetHost[Math.floor(Math.random() * targetHost.length)]
975
- : targetHost;
976
-
977
- // Determine port using function or static value
978
- let targetPort: number;
979
- if (typeof selectedTarget.port === 'function') {
980
- try {
981
- targetPort = selectedTarget.port(routeContext);
982
- if (this.smartProxy.settings.enableDetailedLogging) {
983
- logger.log('info', `Dynamic port mapping from ${record.localPort} to ${targetPort} for connection ${connectionId}`, {
984
- connectionId,
985
- sourcePort: record.localPort,
986
- targetPort,
987
- component: 'route-handler'
988
- });
989
- }
990
- // Store the resolved target port in the context for potential future use
991
- routeContext.targetPort = targetPort;
992
- } catch (err) {
993
- logger.log('error', `Error in port mapping function for connection ${connectionId}: ${err}`, {
994
- connectionId,
995
- error: err,
996
- component: 'route-handler'
997
- });
998
- socket.end();
999
- this.smartProxy.connectionManager.cleanupConnection(record, 'port_mapping_error');
1000
- return;
1001
- }
1002
- } else if (selectedTarget.port === 'preserve') {
1003
- // Use incoming port if port is 'preserve'
1004
- targetPort = record.localPort;
1005
- } else {
1006
- // Use static port from configuration
1007
- targetPort = selectedTarget.port;
1008
- }
1009
-
1010
- // Store the resolved host in the context
1011
- routeContext.targetHost = selectedHost;
1012
-
1013
- // Get effective settings (target overrides route-level settings)
1014
- const effectiveTls = selectedTarget.tls || action.tls;
1015
- const effectiveWebsocket = selectedTarget.websocket || action.websocket;
1016
- const effectiveSendProxyProtocol = selectedTarget.sendProxyProtocol !== undefined
1017
- ? selectedTarget.sendProxyProtocol
1018
- : action.sendProxyProtocol;
1019
-
1020
- // Determine if this needs TLS handling
1021
- if (effectiveTls) {
1022
- switch (effectiveTls.mode) {
1023
- case 'passthrough':
1024
- // For TLS passthrough, just forward directly
1025
- if (this.smartProxy.settings.enableDetailedLogging) {
1026
- logger.log('info', `Using TLS passthrough to ${selectedHost}:${targetPort} for connection ${connectionId}`, {
1027
- connectionId,
1028
- targetHost: selectedHost,
1029
- targetPort,
1030
- component: 'route-handler'
1031
- });
1032
- }
1033
-
1034
- return this.setupDirectConnection(
1035
- socket,
1036
- record,
1037
- record.lockedDomain,
1038
- initialChunk,
1039
- undefined,
1040
- selectedHost,
1041
- targetPort
1042
- );
1043
-
1044
- case 'terminate':
1045
- case 'terminate-and-reencrypt':
1046
- // For TLS termination, use HttpProxy
1047
- if (this.smartProxy.httpProxyBridge.getHttpProxy()) {
1048
- if (this.smartProxy.settings.enableDetailedLogging) {
1049
- logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(selectedTarget.host) ? selectedTarget.host.join(', ') : selectedTarget.host} for connection ${connectionId}`, {
1050
- connectionId,
1051
- targetHost: selectedTarget.host,
1052
- component: 'route-handler'
1053
- });
1054
- }
1055
-
1056
- // If we have an initial chunk with TLS data, start processing it
1057
- if (initialChunk && record.isTLS) {
1058
- this.smartProxy.httpProxyBridge.forwardToHttpProxy(
1059
- connectionId,
1060
- socket,
1061
- record,
1062
- initialChunk,
1063
- this.smartProxy.settings.httpProxyPort || 8443,
1064
- (reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
1065
- );
1066
- return;
1067
- }
1068
-
1069
- // This shouldn't normally happen - we should have TLS data at this point
1070
- logger.log('error', `TLS termination route without TLS data for connection ${connectionId}`, {
1071
- connectionId,
1072
- component: 'route-handler'
1073
- });
1074
- socket.end();
1075
- this.smartProxy.connectionManager.cleanupConnection(record, 'tls_error');
1076
- return;
1077
- } else {
1078
- logger.log('error', `HttpProxy not available for TLS termination for connection ${connectionId}`, {
1079
- connectionId,
1080
- component: 'route-handler'
1081
- });
1082
- socket.end();
1083
- this.smartProxy.connectionManager.cleanupConnection(record, 'no_http_proxy');
1084
- return;
1085
- }
1086
- }
1087
- } else {
1088
- // No TLS settings - check if this port should use HttpProxy
1089
- const isHttpProxyPort = this.smartProxy.settings.useHttpProxy?.includes(record.localPort);
1090
-
1091
- // Debug logging
1092
- if (this.smartProxy.settings.enableDetailedLogging) {
1093
- logger.log('debug', `Checking HttpProxy forwarding: port=${record.localPort}, useHttpProxy=${JSON.stringify(this.smartProxy.settings.useHttpProxy)}, isHttpProxyPort=${isHttpProxyPort}, hasHttpProxy=${!!this.smartProxy.httpProxyBridge.getHttpProxy()}`, {
1094
- connectionId,
1095
- localPort: record.localPort,
1096
- useHttpProxy: this.smartProxy.settings.useHttpProxy,
1097
- isHttpProxyPort,
1098
- hasHttpProxy: !!this.smartProxy.httpProxyBridge.getHttpProxy(),
1099
- component: 'route-handler'
1100
- });
1101
- }
1102
-
1103
- if (isHttpProxyPort && this.smartProxy.httpProxyBridge.getHttpProxy()) {
1104
- // Forward non-TLS connections to HttpProxy if configured
1105
- if (this.smartProxy.settings.enableDetailedLogging) {
1106
- logger.log('info', `Using HttpProxy for non-TLS connection ${connectionId} on port ${record.localPort}`, {
1107
- connectionId,
1108
- port: record.localPort,
1109
- component: 'route-handler'
1110
- });
1111
- }
1112
-
1113
- this.smartProxy.httpProxyBridge.forwardToHttpProxy(
1114
- connectionId,
1115
- socket,
1116
- record,
1117
- initialChunk,
1118
- this.smartProxy.settings.httpProxyPort || 8443,
1119
- (reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
1120
- );
1121
- return;
1122
- } else {
1123
- // Basic forwarding
1124
- if (this.smartProxy.settings.enableDetailedLogging) {
1125
- logger.log('info', `Using basic forwarding to ${Array.isArray(selectedTarget.host) ? selectedTarget.host.join(', ') : selectedTarget.host}:${selectedTarget.port} for connection ${connectionId}`, {
1126
- connectionId,
1127
- targetHost: selectedTarget.host,
1128
- targetPort: selectedTarget.port,
1129
- component: 'route-handler'
1130
- });
1131
- }
1132
-
1133
- // Get the appropriate host value
1134
- let targetHost: string;
1135
-
1136
- if (typeof selectedTarget.host === 'function') {
1137
- // For function-based host, use the same routeContext created earlier
1138
- const hostResult = selectedTarget.host(routeContext);
1139
- targetHost = Array.isArray(hostResult)
1140
- ? hostResult[Math.floor(Math.random() * hostResult.length)]
1141
- : hostResult;
1142
- } else {
1143
- // For static host value
1144
- targetHost = Array.isArray(selectedTarget.host)
1145
- ? selectedTarget.host[Math.floor(Math.random() * selectedTarget.host.length)]
1146
- : selectedTarget.host;
1147
- }
1148
-
1149
- // Determine port - either function-based, static, or preserve incoming port
1150
- let targetPort: number;
1151
- if (typeof selectedTarget.port === 'function') {
1152
- targetPort = selectedTarget.port(routeContext);
1153
- } else if (selectedTarget.port === 'preserve') {
1154
- targetPort = record.localPort;
1155
- } else {
1156
- targetPort = selectedTarget.port;
1157
- }
1158
-
1159
- // Update the connection record and context with resolved values
1160
- record.targetHost = targetHost;
1161
- record.targetPort = targetPort;
1162
-
1163
- return this.setupDirectConnection(
1164
- socket,
1165
- record,
1166
- record.lockedDomain,
1167
- initialChunk,
1168
- undefined,
1169
- targetHost,
1170
- targetPort
1171
- );
1172
- }
1173
- }
1174
- }
1175
-
1176
- /**
1177
- * Handle a socket-handler action for a route
1178
- */
1179
- private async handleSocketHandlerAction(
1180
- socket: plugins.net.Socket | WrappedSocket,
1181
- record: IConnectionRecord,
1182
- route: IRouteConfig,
1183
- initialChunk?: Buffer
1184
- ): Promise<void> {
1185
- const connectionId = record.id;
1186
-
1187
- // Store the route config in the connection record for metrics and other uses
1188
- record.routeConfig = route;
1189
- record.routeId = route.id || route.name || 'unnamed';
1190
-
1191
- // Track connection by route
1192
- this.smartProxy.connectionManager.trackConnectionByRoute(record.routeId, record.id);
1193
-
1194
- if (!route.action.socketHandler) {
1195
- logger.log('error', 'socket-handler action missing socketHandler function', {
1196
- connectionId,
1197
- routeName: route.name,
1198
- component: 'route-handler'
1199
- });
1200
- socket.destroy();
1201
- this.smartProxy.connectionManager.cleanupConnection(record, 'missing_handler');
1202
- return;
1203
- }
1204
-
1205
- // Track event listeners added by the handler so we can clean them up
1206
- const originalOn = socket.on.bind(socket);
1207
- const originalOnce = socket.once.bind(socket);
1208
- const trackedListeners: Array<{event: string; listener: (...args: any[]) => void}> = [];
1209
-
1210
- // Override socket.on to track listeners
1211
- socket.on = function(event: string, listener: (...args: any[]) => void) {
1212
- trackedListeners.push({event, listener});
1213
- return originalOn(event, listener);
1214
- } as any;
1215
-
1216
- // Override socket.once to track listeners
1217
- socket.once = function(event: string, listener: (...args: any[]) => void) {
1218
- trackedListeners.push({event, listener});
1219
- return originalOnce(event, listener);
1220
- } as any;
1221
-
1222
- // Set up automatic cleanup when socket closes
1223
- const cleanupHandler = () => {
1224
- // Remove all tracked listeners
1225
- for (const {event, listener} of trackedListeners) {
1226
- socket.removeListener(event, listener);
1227
- }
1228
- // Restore original methods
1229
- socket.on = originalOn;
1230
- socket.once = originalOnce;
1231
- };
1232
-
1233
- // Listen for socket close to trigger cleanup
1234
- originalOnce('close', cleanupHandler);
1235
- originalOnce('error', cleanupHandler);
1236
-
1237
- // Create route context for the handler
1238
- const routeContext = this.createRouteContext({
1239
- connectionId: record.id,
1240
- port: record.localPort,
1241
- domain: record.lockedDomain,
1242
- clientIp: record.remoteIP,
1243
- serverIp: socket.localAddress || '',
1244
- isTls: record.isTLS || false,
1245
- tlsVersion: record.tlsVersion,
1246
- routeName: route.name,
1247
- routeId: route.id,
1248
- });
1249
-
1250
- try {
1251
- // Call the handler with the appropriate socket (extract underlying if needed)
1252
- const handlerSocket = getUnderlyingSocket(socket);
1253
- const result = route.action.socketHandler(handlerSocket, routeContext);
1254
-
1255
- // Handle async handlers properly
1256
- if (result instanceof Promise) {
1257
- result
1258
- .then(() => {
1259
- // Emit initial chunk after async handler completes
1260
- if (initialChunk && initialChunk.length > 0) {
1261
- socket.emit('data', initialChunk);
1262
- }
1263
- })
1264
- .catch(error => {
1265
- logger.log('error', 'Socket handler error', {
1266
- connectionId,
1267
- routeName: route.name,
1268
- error: error.message,
1269
- component: 'route-handler'
1270
- });
1271
- // Remove all event listeners before destroying to prevent memory leaks
1272
- socket.removeAllListeners();
1273
- if (!socket.destroyed) {
1274
- socket.destroy();
1275
- }
1276
- this.smartProxy.connectionManager.cleanupConnection(record, 'handler_error');
1277
- });
1278
- } else {
1279
- // For sync handlers, emit on next tick
1280
- if (initialChunk && initialChunk.length > 0) {
1281
- process.nextTick(() => {
1282
- socket.emit('data', initialChunk);
1283
- });
1284
- }
1285
- }
1286
- } catch (error) {
1287
- logger.log('error', 'Socket handler error', {
1288
- connectionId,
1289
- routeName: route.name,
1290
- error: error.message,
1291
- component: 'route-handler'
1292
- });
1293
- // Remove all event listeners before destroying to prevent memory leaks
1294
- socket.removeAllListeners();
1295
- if (!socket.destroyed) {
1296
- socket.destroy();
1297
- }
1298
- this.smartProxy.connectionManager.cleanupConnection(record, 'handler_error');
1299
- }
1300
- }
1301
-
1302
-
1303
- /**
1304
- * Sets up a direct connection to the target
1305
- */
1306
- private setupDirectConnection(
1307
- socket: plugins.net.Socket | WrappedSocket,
1308
- record: IConnectionRecord,
1309
- serverName?: string,
1310
- initialChunk?: Buffer,
1311
- overridePort?: number,
1312
- targetHost?: string,
1313
- targetPort?: number
1314
- ): void {
1315
- const connectionId = record.id;
1316
-
1317
- // Determine target host and port if not provided
1318
- const finalTargetHost =
1319
- targetHost || record.targetHost || this.smartProxy.settings.defaults?.target?.host || 'localhost';
1320
-
1321
- // Determine target port
1322
- const finalTargetPort =
1323
- targetPort ||
1324
- record.targetPort ||
1325
- (overridePort !== undefined ? overridePort : this.smartProxy.settings.defaults?.target?.port || 443);
1326
-
1327
- // Update record with final target information
1328
- record.targetHost = finalTargetHost;
1329
- record.targetPort = finalTargetPort;
1330
-
1331
- if (this.smartProxy.settings.enableDetailedLogging) {
1332
- logger.log('info', `Setting up direct connection ${connectionId} to ${finalTargetHost}:${finalTargetPort}`, {
1333
- connectionId,
1334
- targetHost: finalTargetHost,
1335
- targetPort: finalTargetPort,
1336
- component: 'route-handler'
1337
- });
1338
- }
1339
-
1340
- // Setup connection options
1341
- const connectionOptions: plugins.net.NetConnectOpts = {
1342
- host: finalTargetHost,
1343
- port: finalTargetPort,
1344
- };
1345
-
1346
- // Preserve source IP if configured
1347
- if (this.smartProxy.settings.defaults?.preserveSourceIP || this.smartProxy.settings.preserveSourceIP) {
1348
- connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
1349
- }
1350
-
1351
- // Store initial data if provided
1352
- if (initialChunk) {
1353
- // Don't count bytes here - they will be counted when actually forwarded through bidirectional forwarding
1354
- record.pendingData.push(Buffer.from(initialChunk));
1355
- record.pendingDataSize = initialChunk.length;
1356
- }
1357
-
1358
- // Create the target socket with immediate error handling
1359
- const targetSocket = createSocketWithErrorHandler({
1360
- port: finalTargetPort,
1361
- host: finalTargetHost,
1362
- timeout: this.smartProxy.settings.connectionTimeout || 30000, // Connection timeout (default: 30s)
1363
- onError: (error) => {
1364
- // Connection failed - clean up everything immediately
1365
- // Check if connection record is still valid (client might have disconnected)
1366
- if (record.connectionClosed) {
1367
- logger.log('debug', `Backend connection failed but client already disconnected for ${connectionId}`, {
1368
- connectionId,
1369
- errorCode: (error as any).code,
1370
- component: 'route-handler'
1371
- });
1372
- return;
1373
- }
1374
-
1375
- logger.log('error',
1376
- `Connection setup error for ${connectionId} to ${finalTargetHost}:${finalTargetPort}: ${error.message} (${(error as any).code})`,
1377
- {
1378
- connectionId,
1379
- targetHost: finalTargetHost,
1380
- targetPort: finalTargetPort,
1381
- errorMessage: error.message,
1382
- errorCode: (error as any).code,
1383
- component: 'route-handler'
1384
- }
1385
- );
1386
-
1387
- // Log specific error types for easier debugging
1388
- if ((error as any).code === 'ECONNREFUSED') {
1389
- logger.log('error',
1390
- `Connection ${connectionId}: Target ${finalTargetHost}:${finalTargetPort} refused connection. Check if the target service is running and listening on that port.`,
1391
- {
1392
- connectionId,
1393
- targetHost: finalTargetHost,
1394
- targetPort: finalTargetPort,
1395
- recommendation: 'Check if the target service is running and listening on that port.',
1396
- component: 'route-handler'
1397
- }
1398
- );
1399
- }
1400
-
1401
- // Resume the incoming socket to prevent it from hanging
1402
- if (socket && !socket.destroyed) {
1403
- socket.resume();
1404
- }
1405
-
1406
- // Clean up the incoming socket
1407
- if (socket && !socket.destroyed) {
1408
- socket.destroy();
1409
- }
1410
-
1411
- // Clean up the connection record - this is critical!
1412
- this.smartProxy.connectionManager.cleanupConnection(record, `connection_failed_${(error as any).code || 'unknown'}`);
1413
- },
1414
- onConnect: async () => {
1415
- if (this.smartProxy.settings.enableDetailedLogging) {
1416
- logger.log('info', `Connection ${connectionId} established to target ${finalTargetHost}:${finalTargetPort}`, {
1417
- connectionId,
1418
- targetHost: finalTargetHost,
1419
- targetPort: finalTargetPort,
1420
- component: 'route-handler'
1421
- });
1422
- }
1423
-
1424
- // Clear any error listeners added by createSocketWithErrorHandler
1425
- targetSocket.removeAllListeners('error');
1426
-
1427
- // Add the normal error handler for established connections
1428
- targetSocket.on('error', this.smartProxy.connectionManager.handleError('outgoing', record));
1429
-
1430
- // Check if we should send PROXY protocol header
1431
- const shouldSendProxyProtocol = record.routeConfig?.action?.sendProxyProtocol ||
1432
- this.smartProxy.settings.sendProxyProtocol;
1433
-
1434
- if (shouldSendProxyProtocol) {
1435
- try {
1436
- // Generate PROXY protocol header
1437
- const proxyInfo = {
1438
- protocol: (record.remoteIP.includes(':') ? 'TCP6' : 'TCP4') as 'TCP4' | 'TCP6',
1439
- sourceIP: record.remoteIP,
1440
- sourcePort: record.remotePort || socket.remotePort || 0,
1441
- destinationIP: socket.localAddress || '',
1442
- destinationPort: socket.localPort || 0
1443
- };
1444
-
1445
- const proxyHeader = ProxyProtocolParser.generate(proxyInfo);
1446
-
1447
- // Note: PROXY protocol headers are sent to the backend, not to the client
1448
- // They are internal protocol overhead and shouldn't be counted in client-facing metrics
1449
-
1450
- // Send PROXY protocol header first
1451
- await new Promise<void>((resolve, reject) => {
1452
- targetSocket.write(proxyHeader, (err) => {
1453
- if (err) {
1454
- logger.log('error', `Failed to send PROXY protocol header`, {
1455
- connectionId,
1456
- error: err.message,
1457
- component: 'route-handler'
1458
- });
1459
- reject(err);
1460
- } else {
1461
- logger.log('info', `PROXY protocol header sent to backend`, {
1462
- connectionId,
1463
- targetHost: finalTargetHost,
1464
- targetPort: finalTargetPort,
1465
- sourceIP: proxyInfo.sourceIP,
1466
- sourcePort: proxyInfo.sourcePort,
1467
- component: 'route-handler'
1468
- });
1469
- resolve();
1470
- }
1471
- });
1472
- });
1473
- } catch (error) {
1474
- logger.log('error', `Error sending PROXY protocol header`, {
1475
- connectionId,
1476
- error: error.message,
1477
- component: 'route-handler'
1478
- });
1479
- // Continue anyway - don't break the connection
1480
- }
1481
- }
1482
-
1483
- // Flush any pending data to target
1484
- if (record.pendingData.length > 0) {
1485
- const combinedData = Buffer.concat(record.pendingData);
1486
-
1487
- if (this.smartProxy.settings.enableDetailedLogging) {
1488
- console.log(
1489
- `[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
1490
- );
1491
- }
1492
-
1493
- // Record the initial chunk bytes for metrics
1494
- record.bytesReceived += combinedData.length;
1495
- if (this.smartProxy.metricsCollector) {
1496
- this.smartProxy.metricsCollector.recordBytes(record.id, combinedData.length, 0);
1497
- }
1498
-
1499
- // Write pending data immediately
1500
- targetSocket.write(combinedData, (err) => {
1501
- if (err) {
1502
- logger.log('error', `Error writing pending data to target for connection ${connectionId}: ${err.message}`, {
1503
- connectionId,
1504
- error: err.message,
1505
- component: 'route-handler'
1506
- });
1507
- return this.smartProxy.connectionManager.cleanupConnection(record, 'write_error');
1508
- }
1509
- });
1510
-
1511
- // Clear the buffer now that we've processed it
1512
- record.pendingData = [];
1513
- record.pendingDataSize = 0;
1514
- }
1515
-
1516
- // Use centralized bidirectional forwarding setup
1517
- // Extract underlying sockets for socket-utils functions
1518
- const incomingSocket = getUnderlyingSocket(socket);
1519
-
1520
- setupBidirectionalForwarding(incomingSocket, targetSocket, {
1521
- onClientData: (chunk) => {
1522
- record.bytesReceived += chunk.length;
1523
- this.smartProxy.timeoutManager.updateActivity(record);
1524
-
1525
- // Record bytes for metrics
1526
- if (this.smartProxy.metricsCollector) {
1527
- this.smartProxy.metricsCollector.recordBytes(record.id, chunk.length, 0);
1528
- }
1529
- },
1530
- onServerData: (chunk) => {
1531
- record.bytesSent += chunk.length;
1532
- this.smartProxy.timeoutManager.updateActivity(record);
1533
-
1534
- // Record bytes for metrics
1535
- if (this.smartProxy.metricsCollector) {
1536
- this.smartProxy.metricsCollector.recordBytes(record.id, 0, chunk.length);
1537
- }
1538
- },
1539
- onCleanup: (reason) => {
1540
- this.smartProxy.connectionManager.cleanupConnection(record, reason);
1541
- },
1542
- enableHalfOpen: false // Default: close both when one closes (required for proxy chains)
1543
- });
1544
-
1545
- // Apply timeouts using TimeoutManager
1546
- const timeout = this.smartProxy.timeoutManager.getEffectiveInactivityTimeout(record);
1547
- // Skip timeout for immortal connections (MAX_SAFE_INTEGER would cause issues)
1548
- if (timeout !== Number.MAX_SAFE_INTEGER) {
1549
- const safeTimeout = this.smartProxy.timeoutManager.ensureSafeTimeout(timeout);
1550
- socket.setTimeout(safeTimeout);
1551
- targetSocket.setTimeout(safeTimeout);
1552
- }
1553
-
1554
- // Log successful connection
1555
- logger.log('info',
1556
- `Connection established: ${record.remoteIP} -> ${finalTargetHost}:${finalTargetPort}` +
1557
- `${serverName ? ` (SNI: ${serverName})` : record.lockedDomain ? ` (Domain: ${record.lockedDomain})` : ''}`,
1558
- {
1559
- remoteIP: record.remoteIP,
1560
- targetHost: finalTargetHost,
1561
- targetPort: finalTargetPort,
1562
- sni: serverName || undefined,
1563
- domain: !serverName && record.lockedDomain ? record.lockedDomain : undefined,
1564
- component: 'route-handler'
1565
- }
1566
- );
1567
-
1568
- // Add TLS renegotiation handler if needed
1569
- if (serverName) {
1570
- // Create connection info object for the existing connection
1571
- const connInfo = {
1572
- sourceIp: record.remoteIP,
1573
- sourcePort: record.incoming.remotePort || 0,
1574
- destIp: record.incoming.localAddress || '',
1575
- destPort: record.incoming.localPort || 0,
1576
- };
1577
-
1578
- // Create a renegotiation handler function
1579
- const renegotiationHandler = this.smartProxy.tlsManager.createRenegotiationHandler(
1580
- connectionId,
1581
- serverName,
1582
- connInfo,
1583
- (_connectionId, reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
1584
- );
1585
-
1586
- // Store the handler in the connection record so we can remove it during cleanup
1587
- record.renegotiationHandler = renegotiationHandler;
1588
-
1589
- // Add the handler to the socket
1590
- socket.on('data', renegotiationHandler);
1591
-
1592
- if (this.smartProxy.settings.enableDetailedLogging) {
1593
- logger.log('info', `TLS renegotiation handler installed for connection ${connectionId} with SNI ${serverName}`, {
1594
- connectionId,
1595
- serverName,
1596
- component: 'route-handler'
1597
- });
1598
- }
1599
- }
1600
-
1601
- // Set connection timeout
1602
- record.cleanupTimer = this.smartProxy.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
1603
- logger.log('warn', `Connection ${connectionId} from ${record.remoteIP} exceeded max lifetime, forcing cleanup`, {
1604
- connectionId,
1605
- remoteIP: record.remoteIP,
1606
- component: 'route-handler'
1607
- });
1608
- this.smartProxy.connectionManager.cleanupConnection(record, reason);
1609
- });
1610
-
1611
- // Mark TLS handshake as complete for TLS connections
1612
- if (record.isTLS) {
1613
- record.tlsHandshakeComplete = true;
1614
- }
1615
- }
1616
- });
1617
-
1618
- // Set outgoing socket immediately so it can be cleaned up if client disconnects
1619
- record.outgoing = targetSocket;
1620
- record.outgoingStartTime = Date.now();
1621
-
1622
- // Apply socket optimizations
1623
- targetSocket.setNoDelay(this.smartProxy.settings.noDelay);
1624
-
1625
- // Apply keep-alive settings if enabled
1626
- if (this.smartProxy.settings.keepAlive) {
1627
- targetSocket.setKeepAlive(true, this.smartProxy.settings.keepAliveInitialDelay);
1628
-
1629
- // Apply enhanced TCP keep-alive options if enabled
1630
- if (this.smartProxy.settings.enableKeepAliveProbes) {
1631
- try {
1632
- if ('setKeepAliveProbes' in targetSocket) {
1633
- (targetSocket as any).setKeepAliveProbes(10);
1634
- }
1635
- if ('setKeepAliveInterval' in targetSocket) {
1636
- (targetSocket as any).setKeepAliveInterval(1000);
1637
- }
1638
- } catch (err) {
1639
- // Ignore errors - these are optional enhancements
1640
- if (this.smartProxy.settings.enableDetailedLogging) {
1641
- logger.log('warn', `Enhanced TCP keep-alive not supported for outgoing socket on connection ${connectionId}: ${err}`, {
1642
- connectionId,
1643
- error: err,
1644
- component: 'route-handler'
1645
- });
1646
- }
1647
- }
1648
- }
1649
- }
1650
-
1651
- // Setup error handlers for incoming socket
1652
- socket.on('error', this.smartProxy.connectionManager.handleError('incoming', record));
1653
-
1654
- // Handle timeouts with keep-alive awareness
1655
- socket.on('timeout', () => {
1656
- // For keep-alive connections, just log a warning instead of closing
1657
- if (record.hasKeepAlive) {
1658
- logger.log('warn', `Timeout event on incoming keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}. Connection preserved.`, {
1659
- connectionId,
1660
- remoteIP: record.remoteIP,
1661
- timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
1662
- status: 'Connection preserved',
1663
- component: 'route-handler'
1664
- });
1665
- return;
1666
- }
1667
-
1668
- // For non-keep-alive connections, proceed with normal cleanup
1669
- logger.log('warn', `Timeout on incoming side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}`, {
1670
- connectionId,
1671
- remoteIP: record.remoteIP,
1672
- timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
1673
- component: 'route-handler'
1674
- });
1675
- if (record.incomingTerminationReason === null) {
1676
- record.incomingTerminationReason = 'timeout';
1677
- this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'timeout');
1678
- }
1679
- this.smartProxy.connectionManager.cleanupConnection(record, 'timeout_incoming');
1680
- });
1681
-
1682
- targetSocket.on('timeout', () => {
1683
- // For keep-alive connections, just log a warning instead of closing
1684
- if (record.hasKeepAlive) {
1685
- logger.log('warn', `Timeout event on outgoing keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}. Connection preserved.`, {
1686
- connectionId,
1687
- remoteIP: record.remoteIP,
1688
- timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
1689
- status: 'Connection preserved',
1690
- component: 'route-handler'
1691
- });
1692
- return;
1693
- }
1694
-
1695
- // For non-keep-alive connections, proceed with normal cleanup
1696
- logger.log('warn', `Timeout on outgoing side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}`, {
1697
- connectionId,
1698
- remoteIP: record.remoteIP,
1699
- timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
1700
- component: 'route-handler'
1701
- });
1702
- if (record.outgoingTerminationReason === null) {
1703
- record.outgoingTerminationReason = 'timeout';
1704
- this.smartProxy.connectionManager.incrementTerminationStat('outgoing', 'timeout');
1705
- }
1706
- this.smartProxy.connectionManager.cleanupConnection(record, 'timeout_outgoing');
1707
- });
1708
-
1709
- // Apply socket timeouts
1710
- this.smartProxy.timeoutManager.applySocketTimeouts(record);
1711
- }
1712
- }