fix(metrics): improve metrics

This commit is contained in:
Juergen Kunz
2025-06-22 22:28:37 +00:00
parent de1269665a
commit 131a454b28
16 changed files with 1389 additions and 502 deletions

View File

@ -4,23 +4,16 @@ import { logger } from '../../core/utils/logger.js';
// Route checking functions have been removed
import type { IRouteConfig, IRouteAction } from './models/route-types.js';
import type { IRouteContext } from '../../core/models/route-context.js';
import { ConnectionManager } from './connection-manager.js';
import { SecurityManager } from './security-manager.js';
import { TlsManager } from './tls-manager.js';
import { HttpProxyBridge } from './http-proxy-bridge.js';
import { TimeoutManager } from './timeout-manager.js';
import { SharedRouteManager as RouteManager } from '../../core/routing/route-manager.js';
import { cleanupSocket, setupSocketHandlers, createSocketWithErrorHandler, setupBidirectionalForwarding } from '../../core/utils/socket-utils.js';
import { WrappedSocket } from '../../core/models/wrapped-socket.js';
import { getUnderlyingSocket } from '../../core/models/socket-types.js';
import { ProxyProtocolParser } from '../../core/utils/proxy-protocol.js';
import type { SmartProxy } from './smart-proxy.js';
/**
* Handles new connection processing and setup logic with support for route-based configuration
*/
export class RouteConnectionHandler {
private settings: ISmartProxyOptions;
// Note: Route context caching was considered but not implemented
// as route contexts are lightweight and should be created fresh
// for each connection to ensure accurate context data
@ -29,16 +22,8 @@ export class RouteConnectionHandler {
public newConnectionSubject = new plugins.smartrx.rxjs.Subject<IConnectionRecord>();
constructor(
settings: ISmartProxyOptions,
private connectionManager: ConnectionManager,
private securityManager: SecurityManager,
private tlsManager: TlsManager,
private httpProxyBridge: HttpProxyBridge,
private timeoutManager: TimeoutManager,
private routeManager: RouteManager
) {
this.settings = settings;
}
private smartProxy: SmartProxy
) {}
/**
@ -93,7 +78,7 @@ export class RouteConnectionHandler {
const wrappedSocket = new WrappedSocket(socket);
// If this is from a trusted proxy, log it
if (this.settings.proxyIPs?.includes(remoteIP)) {
if (this.smartProxy.settings.proxyIPs?.includes(remoteIP)) {
logger.log('debug', `Connection from trusted proxy ${remoteIP}, PROXY protocol parsing will be enabled`, {
remoteIP,
component: 'route-handler'
@ -102,7 +87,7 @@ export class RouteConnectionHandler {
// Validate IP against rate limits and connection limits
// Note: For wrapped sockets, this will use the underlying socket IP until PROXY protocol is parsed
const ipValidation = this.securityManager.validateIP(wrappedSocket.remoteAddress || '');
const ipValidation = this.smartProxy.securityManager.validateIP(wrappedSocket.remoteAddress || '');
if (!ipValidation.allowed) {
logger.log('warn', `Connection rejected`, { remoteIP: wrappedSocket.remoteAddress, reason: ipValidation.reason, component: 'route-handler' });
cleanupSocket(wrappedSocket.socket, `rejected-${ipValidation.reason}`, { immediate: true });
@ -110,7 +95,7 @@ export class RouteConnectionHandler {
}
// Create a new connection record with the wrapped socket
const record = this.connectionManager.createConnection(wrappedSocket);
const record = this.smartProxy.connectionManager.createConnection(wrappedSocket);
if (!record) {
// Connection was rejected due to limit - socket already destroyed by connection manager
return;
@ -122,15 +107,15 @@ export class RouteConnectionHandler {
// Apply socket optimizations (apply to underlying socket)
const underlyingSocket = wrappedSocket.socket;
underlyingSocket.setNoDelay(this.settings.noDelay);
underlyingSocket.setNoDelay(this.smartProxy.settings.noDelay);
// Apply keep-alive settings if enabled
if (this.settings.keepAlive) {
underlyingSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
if (this.smartProxy.settings.keepAlive) {
underlyingSocket.setKeepAlive(true, this.smartProxy.settings.keepAliveInitialDelay);
record.hasKeepAlive = true;
// Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) {
if (this.smartProxy.settings.enableKeepAliveProbes) {
try {
// These are platform-specific and may not be available
if ('setKeepAliveProbes' in underlyingSocket) {
@ -141,34 +126,34 @@ export class RouteConnectionHandler {
}
} catch (err) {
// Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('warn', `Enhanced TCP keep-alive settings not supported`, { connectionId, error: err, component: 'route-handler' });
}
}
}
}
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info',
`New connection from ${remoteIP} on port ${localPort}. ` +
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
`Active connections: ${this.connectionManager.getConnectionCount()}`,
`Active connections: ${this.smartProxy.connectionManager.getConnectionCount()}`,
{
connectionId,
remoteIP,
localPort,
keepAlive: record.hasKeepAlive ? 'Enabled' : 'Disabled',
activeConnections: this.connectionManager.getConnectionCount(),
activeConnections: this.smartProxy.connectionManager.getConnectionCount(),
component: 'route-handler'
}
);
} else {
logger.log('info',
`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`,
`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.smartProxy.connectionManager.getConnectionCount()}`,
{
remoteIP,
localPort,
activeConnections: this.connectionManager.getConnectionCount(),
activeConnections: this.smartProxy.connectionManager.getConnectionCount(),
component: 'route-handler'
}
);
@ -187,10 +172,10 @@ export class RouteConnectionHandler {
let initialDataReceived = false;
// Check if any routes on this port require TLS handling
const allRoutes = this.routeManager.getRoutes();
const allRoutes = this.smartProxy.routeManager.getRoutes();
const needsTlsHandling = allRoutes.some(route => {
// Check if route matches this port
const matchesPort = this.routeManager.getRoutesForPort(localPort).includes(route);
const matchesPort = this.smartProxy.routeManager.getRoutesForPort(localPort).includes(route);
return matchesPort &&
route.action.type === 'forward' &&
@ -229,7 +214,7 @@ export class RouteConnectionHandler {
}
// Always cleanup the connection record
this.connectionManager.cleanupConnection(record, reason);
this.smartProxy.connectionManager.cleanupConnection(record, reason);
},
undefined, // Use default timeout handler
'immediate-route-client'
@ -244,9 +229,9 @@ export class RouteConnectionHandler {
// Set an initial timeout for handshake data
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
if (!initialDataReceived) {
logger.log('warn', `No initial data received from ${record.remoteIP} after ${this.settings.initialDataTimeout}ms for connection ${connectionId}`, {
logger.log('warn', `No initial data received from ${record.remoteIP} after ${this.smartProxy.settings.initialDataTimeout}ms for connection ${connectionId}`, {
connectionId,
timeout: this.settings.initialDataTimeout,
timeout: this.smartProxy.settings.initialDataTimeout,
remoteIP: record.remoteIP,
component: 'route-handler'
});
@ -260,14 +245,14 @@ export class RouteConnectionHandler {
});
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'initial_timeout';
this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
}
socket.end();
this.connectionManager.cleanupConnection(record, 'initial_timeout');
this.smartProxy.connectionManager.cleanupConnection(record, 'initial_timeout');
}
}, 30000);
}
}, this.settings.initialDataTimeout!);
}, this.smartProxy.settings.initialDataTimeout!);
// Make sure timeout doesn't keep the process alive
if (initialTimeout.unref) {
@ -275,7 +260,7 @@ export class RouteConnectionHandler {
}
// Set up error handler
socket.on('error', this.connectionManager.handleError('incoming', record));
socket.on('error', this.smartProxy.connectionManager.handleError('incoming', record));
// Add close/end handlers to catch immediate disconnections
socket.once('close', () => {
@ -289,7 +274,7 @@ export class RouteConnectionHandler {
clearTimeout(initialTimeout);
initialTimeout = null;
}
this.connectionManager.cleanupConnection(record, 'closed_before_data');
this.smartProxy.connectionManager.cleanupConnection(record, 'closed_before_data');
}
});
@ -311,7 +296,7 @@ export class RouteConnectionHandler {
// Handler for processing initial data (after potential PROXY protocol)
const processInitialData = (chunk: Buffer) => {
// Block non-TLS connections on port 443
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
if (!this.smartProxy.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
logger.log('warn', `Non-TLS connection ${connectionId} detected on port 443. Terminating connection - only TLS traffic is allowed on standard HTTPS port.`, {
connectionId,
message: 'Terminating connection - only TLS traffic is allowed on standard HTTPS port.',
@ -319,20 +304,20 @@ export class RouteConnectionHandler {
});
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'non_tls_blocked';
this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
}
socket.end();
this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
this.smartProxy.connectionManager.cleanupConnection(record, 'non_tls_blocked');
return;
}
// Check if this looks like a TLS handshake
let serverName = '';
if (this.tlsManager.isTlsHandshake(chunk)) {
if (this.smartProxy.tlsManager.isTlsHandshake(chunk)) {
record.isTLS = true;
// Check for ClientHello to extract SNI
if (this.tlsManager.isClientHello(chunk)) {
if (this.smartProxy.tlsManager.isClientHello(chunk)) {
// Create connection info for SNI extraction
const connInfo = {
sourceIp: record.remoteIP,
@ -342,20 +327,20 @@ export class RouteConnectionHandler {
};
// Extract SNI
serverName = this.tlsManager.extractSNI(chunk, connInfo) || '';
serverName = this.smartProxy.tlsManager.extractSNI(chunk, connInfo) || '';
// Lock the connection to the negotiated SNI
record.lockedDomain = serverName;
// Check if we should reject connections without SNI
if (!serverName && this.settings.allowSessionTicket === false) {
if (!serverName && this.smartProxy.settings.allowSessionTicket === false) {
logger.log('warn', `No SNI detected in TLS ClientHello for connection ${connectionId}; sending TLS alert`, {
connectionId,
component: 'route-handler'
});
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
this.connectionManager.incrementTerminationStat(
this.smartProxy.connectionManager.incrementTerminationStat(
'incoming',
'session_ticket_blocked_no_sni'
);
@ -369,11 +354,11 @@ export class RouteConnectionHandler {
} catch {
socket.end();
}
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
this.smartProxy.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
return;
}
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `TLS connection with SNI`, {
connectionId,
serverName: serverName || '(empty)',
@ -399,7 +384,7 @@ export class RouteConnectionHandler {
record.hasReceivedInitialData = true;
// Check if this is from a trusted proxy and might have PROXY protocol
if (this.settings.proxyIPs?.includes(socket.remoteAddress || '') && this.settings.acceptProxyProtocol !== false) {
if (this.smartProxy.settings.proxyIPs?.includes(socket.remoteAddress || '') && this.smartProxy.settings.acceptProxyProtocol !== false) {
// Check if this starts with PROXY protocol
if (chunk.toString('ascii', 0, Math.min(6, chunk.length)).startsWith('PROXY ')) {
try {
@ -463,7 +448,7 @@ export class RouteConnectionHandler {
const remoteIP = record.remoteIP;
// Check if this is an HTTP proxy port
const isHttpProxyPort = this.settings.useHttpProxy?.includes(localPort);
const isHttpProxyPort = this.smartProxy.settings.useHttpProxy?.includes(localPort);
// For HTTP proxy ports without TLS, skip domain check since domain info comes from HTTP headers
const skipDomainCheck = isHttpProxyPort && !record.isTLS;
@ -482,7 +467,7 @@ export class RouteConnectionHandler {
};
// Find matching route
const routeMatch = this.routeManager.findMatchingRoute(routeContext);
const routeMatch = this.smartProxy.routeManager.findMatchingRoute(routeContext);
if (!routeMatch) {
logger.log('warn', `No route found for ${serverName || 'connection'} on port ${localPort} (connection: ${connectionId})`, {
@ -499,10 +484,10 @@ export class RouteConnectionHandler {
});
// Check default security settings
const defaultSecuritySettings = this.settings.defaults?.security;
const defaultSecuritySettings = this.smartProxy.settings.defaults?.security;
if (defaultSecuritySettings) {
if (defaultSecuritySettings.ipAllowList && defaultSecuritySettings.ipAllowList.length > 0) {
const isAllowed = this.securityManager.isIPAuthorized(
const isAllowed = this.smartProxy.securityManager.isIPAuthorized(
remoteIP,
defaultSecuritySettings.ipAllowList,
defaultSecuritySettings.ipBlockList || []
@ -515,17 +500,17 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'ip_blocked');
this.smartProxy.connectionManager.cleanupConnection(record, 'ip_blocked');
return;
}
}
}
// Setup direct connection with default settings
if (this.settings.defaults?.target) {
if (this.smartProxy.settings.defaults?.target) {
// Use defaults from configuration
const targetHost = this.settings.defaults.target.host;
const targetPort = this.settings.defaults.target.port;
const targetHost = this.smartProxy.settings.defaults.target.host;
const targetPort = this.smartProxy.settings.defaults.target.port;
return this.setupDirectConnection(
socket,
@ -543,7 +528,7 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'no_default_target');
this.smartProxy.connectionManager.cleanupConnection(record, 'no_default_target');
return;
}
}
@ -551,7 +536,7 @@ export class RouteConnectionHandler {
// A matching route was found
const route = routeMatch.route;
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Route matched`, {
connectionId,
routeName: route.name || 'unnamed',
@ -565,7 +550,7 @@ export class RouteConnectionHandler {
if (route.security) {
// Check IP allow/block lists
if (route.security.ipAllowList || route.security.ipBlockList) {
const isIPAllowed = this.securityManager.isIPAuthorized(
const isIPAllowed = this.smartProxy.securityManager.isIPAuthorized(
remoteIP,
route.security.ipAllowList || [],
route.security.ipBlockList || []
@ -579,7 +564,7 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'route_ip_blocked');
this.smartProxy.connectionManager.cleanupConnection(record, 'route_ip_blocked');
return;
}
}
@ -588,7 +573,7 @@ export class RouteConnectionHandler {
if (route.security.maxConnections !== undefined) {
// TODO: Implement per-route connection tracking
// For now, log that this feature is not yet implemented
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('warn', `Route ${route.name} has maxConnections=${route.security.maxConnections} configured but per-route connection limits are not yet implemented`, {
connectionId,
routeName: route.name,
@ -633,7 +618,7 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'unknown_action');
this.smartProxy.connectionManager.cleanupConnection(record, 'unknown_action');
}
}
@ -658,7 +643,7 @@ export class RouteConnectionHandler {
// The application should NOT interfere with these connections
// Log the connection for monitoring purposes
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `NFTables forwarding (kernel-level)`, {
connectionId: record.id,
source: `${record.remoteIP}:${socket.remotePort}`,
@ -680,7 +665,7 @@ export class RouteConnectionHandler {
// Additional NFTables-specific logging if configured
if (action.nftables) {
const nftConfig = action.nftables;
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `NFTables config`, {
connectionId: record.id,
protocol: nftConfig.protocol || 'tcp',
@ -701,7 +686,7 @@ export class RouteConnectionHandler {
// Set up cleanup when the socket eventually closes
socket.once('close', () => {
this.connectionManager.cleanupConnection(record, 'nftables_closed');
this.smartProxy.connectionManager.cleanupConnection(record, 'nftables_closed');
});
return;
@ -714,7 +699,7 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'missing_target');
this.smartProxy.connectionManager.cleanupConnection(record, 'missing_target');
return;
}
@ -738,7 +723,7 @@ export class RouteConnectionHandler {
if (typeof action.target.host === 'function') {
try {
targetHost = action.target.host(routeContext);
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Dynamic host resolved to ${Array.isArray(targetHost) ? targetHost.join(', ') : targetHost} for connection ${connectionId}`, {
connectionId,
targetHost: Array.isArray(targetHost) ? targetHost.join(', ') : targetHost,
@ -752,7 +737,7 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'host_mapping_error');
this.smartProxy.connectionManager.cleanupConnection(record, 'host_mapping_error');
return;
}
} else {
@ -769,7 +754,7 @@ export class RouteConnectionHandler {
if (typeof action.target.port === 'function') {
try {
targetPort = action.target.port(routeContext);
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Dynamic port mapping from ${record.localPort} to ${targetPort} for connection ${connectionId}`, {
connectionId,
sourcePort: record.localPort,
@ -786,7 +771,7 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'port_mapping_error');
this.smartProxy.connectionManager.cleanupConnection(record, 'port_mapping_error');
return;
}
} else if (action.target.port === 'preserve') {
@ -805,7 +790,7 @@ export class RouteConnectionHandler {
switch (action.tls.mode) {
case 'passthrough':
// For TLS passthrough, just forward directly
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Using TLS passthrough to ${selectedHost}:${targetPort} for connection ${connectionId}`, {
connectionId,
targetHost: selectedHost,
@ -827,8 +812,8 @@ export class RouteConnectionHandler {
case 'terminate':
case 'terminate-and-reencrypt':
// For TLS termination, use HttpProxy
if (this.httpProxyBridge.getHttpProxy()) {
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.httpProxyBridge.getHttpProxy()) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Using HttpProxy for TLS termination to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host} for connection ${connectionId}`, {
connectionId,
targetHost: action.target.host,
@ -838,13 +823,13 @@ export class RouteConnectionHandler {
// If we have an initial chunk with TLS data, start processing it
if (initialChunk && record.isTLS) {
this.httpProxyBridge.forwardToHttpProxy(
this.smartProxy.httpProxyBridge.forwardToHttpProxy(
connectionId,
socket,
record,
initialChunk,
this.settings.httpProxyPort || 8443,
(reason) => this.connectionManager.cleanupConnection(record, reason)
this.smartProxy.settings.httpProxyPort || 8443,
(reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
);
return;
}
@ -855,7 +840,7 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'tls_error');
this.smartProxy.connectionManager.cleanupConnection(record, 'tls_error');
return;
} else {
logger.log('error', `HttpProxy not available for TLS termination for connection ${connectionId}`, {
@ -863,29 +848,29 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.end();
this.connectionManager.cleanupConnection(record, 'no_http_proxy');
this.smartProxy.connectionManager.cleanupConnection(record, 'no_http_proxy');
return;
}
}
} else {
// No TLS settings - check if this port should use HttpProxy
const isHttpProxyPort = this.settings.useHttpProxy?.includes(record.localPort);
const isHttpProxyPort = this.smartProxy.settings.useHttpProxy?.includes(record.localPort);
// Debug logging
if (this.settings.enableDetailedLogging) {
logger.log('debug', `Checking HttpProxy forwarding: port=${record.localPort}, useHttpProxy=${JSON.stringify(this.settings.useHttpProxy)}, isHttpProxyPort=${isHttpProxyPort}, hasHttpProxy=${!!this.httpProxyBridge.getHttpProxy()}`, {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('debug', `Checking HttpProxy forwarding: port=${record.localPort}, useHttpProxy=${JSON.stringify(this.smartProxy.settings.useHttpProxy)}, isHttpProxyPort=${isHttpProxyPort}, hasHttpProxy=${!!this.smartProxy.httpProxyBridge.getHttpProxy()}`, {
connectionId,
localPort: record.localPort,
useHttpProxy: this.settings.useHttpProxy,
useHttpProxy: this.smartProxy.settings.useHttpProxy,
isHttpProxyPort,
hasHttpProxy: !!this.httpProxyBridge.getHttpProxy(),
hasHttpProxy: !!this.smartProxy.httpProxyBridge.getHttpProxy(),
component: 'route-handler'
});
}
if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) {
if (isHttpProxyPort && this.smartProxy.httpProxyBridge.getHttpProxy()) {
// Forward non-TLS connections to HttpProxy if configured
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Using HttpProxy for non-TLS connection ${connectionId} on port ${record.localPort}`, {
connectionId,
port: record.localPort,
@ -893,18 +878,18 @@ export class RouteConnectionHandler {
});
}
this.httpProxyBridge.forwardToHttpProxy(
this.smartProxy.httpProxyBridge.forwardToHttpProxy(
connectionId,
socket,
record,
initialChunk,
this.settings.httpProxyPort || 8443,
(reason) => this.connectionManager.cleanupConnection(record, reason)
this.smartProxy.settings.httpProxyPort || 8443,
(reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
);
return;
} else {
// Basic forwarding
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Using basic forwarding to ${Array.isArray(action.target.host) ? action.target.host.join(', ') : action.target.host}:${action.target.port} for connection ${connectionId}`, {
connectionId,
targetHost: action.target.host,
@ -977,7 +962,7 @@ export class RouteConnectionHandler {
component: 'route-handler'
});
socket.destroy();
this.connectionManager.cleanupConnection(record, 'missing_handler');
this.smartProxy.connectionManager.cleanupConnection(record, 'missing_handler');
return;
}
@ -1052,7 +1037,7 @@ export class RouteConnectionHandler {
if (!socket.destroyed) {
socket.destroy();
}
this.connectionManager.cleanupConnection(record, 'handler_error');
this.smartProxy.connectionManager.cleanupConnection(record, 'handler_error');
});
} else {
// For sync handlers, emit on next tick
@ -1074,7 +1059,7 @@ export class RouteConnectionHandler {
if (!socket.destroyed) {
socket.destroy();
}
this.connectionManager.cleanupConnection(record, 'handler_error');
this.smartProxy.connectionManager.cleanupConnection(record, 'handler_error');
}
}
@ -1095,19 +1080,19 @@ export class RouteConnectionHandler {
// Determine target host and port if not provided
const finalTargetHost =
targetHost || record.targetHost || this.settings.defaults?.target?.host || 'localhost';
targetHost || record.targetHost || this.smartProxy.settings.defaults?.target?.host || 'localhost';
// Determine target port
const finalTargetPort =
targetPort ||
record.targetPort ||
(overridePort !== undefined ? overridePort : this.settings.defaults?.target?.port || 443);
(overridePort !== undefined ? overridePort : this.smartProxy.settings.defaults?.target?.port || 443);
// Update record with final target information
record.targetHost = finalTargetHost;
record.targetPort = finalTargetPort;
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Setting up direct connection ${connectionId} to ${finalTargetHost}:${finalTargetPort}`, {
connectionId,
targetHost: finalTargetHost,
@ -1123,7 +1108,7 @@ export class RouteConnectionHandler {
};
// Preserve source IP if configured
if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) {
if (this.smartProxy.settings.defaults?.preserveSourceIP || this.smartProxy.settings.preserveSourceIP) {
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
}
@ -1132,13 +1117,18 @@ export class RouteConnectionHandler {
record.bytesReceived += initialChunk.length;
record.pendingData.push(Buffer.from(initialChunk));
record.pendingDataSize = initialChunk.length;
// Record bytes for metrics
if (this.smartProxy.metricsCollector) {
this.smartProxy.metricsCollector.recordBytes(record.id, initialChunk.length, 0);
}
}
// Create the target socket with immediate error handling
const targetSocket = createSocketWithErrorHandler({
port: finalTargetPort,
host: finalTargetHost,
timeout: this.settings.connectionTimeout || 30000, // Connection timeout (default: 30s)
timeout: this.smartProxy.settings.connectionTimeout || 30000, // Connection timeout (default: 30s)
onError: (error) => {
// Connection failed - clean up everything immediately
// Check if connection record is still valid (client might have disconnected)
@ -1188,10 +1178,10 @@ export class RouteConnectionHandler {
}
// Clean up the connection record - this is critical!
this.connectionManager.cleanupConnection(record, `connection_failed_${(error as any).code || 'unknown'}`);
this.smartProxy.connectionManager.cleanupConnection(record, `connection_failed_${(error as any).code || 'unknown'}`);
},
onConnect: async () => {
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `Connection ${connectionId} established to target ${finalTargetHost}:${finalTargetPort}`, {
connectionId,
targetHost: finalTargetHost,
@ -1204,11 +1194,11 @@ export class RouteConnectionHandler {
targetSocket.removeAllListeners('error');
// Add the normal error handler for established connections
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
targetSocket.on('error', this.smartProxy.connectionManager.handleError('outgoing', record));
// Check if we should send PROXY protocol header
const shouldSendProxyProtocol = record.routeConfig?.action?.sendProxyProtocol ||
this.settings.sendProxyProtocol;
this.smartProxy.settings.sendProxyProtocol;
if (shouldSendProxyProtocol) {
try {
@ -1260,7 +1250,7 @@ export class RouteConnectionHandler {
if (record.pendingData.length > 0) {
const combinedData = Buffer.concat(record.pendingData);
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
);
@ -1274,7 +1264,7 @@ export class RouteConnectionHandler {
error: err.message,
component: 'route-handler'
});
return this.connectionManager.cleanupConnection(record, 'write_error');
return this.smartProxy.connectionManager.cleanupConnection(record, 'write_error');
}
});
@ -1290,22 +1280,32 @@ export class RouteConnectionHandler {
setupBidirectionalForwarding(incomingSocket, targetSocket, {
onClientData: (chunk) => {
record.bytesReceived += chunk.length;
this.timeoutManager.updateActivity(record);
this.smartProxy.timeoutManager.updateActivity(record);
// Record bytes for metrics
if (this.smartProxy.metricsCollector) {
this.smartProxy.metricsCollector.recordBytes(record.id, chunk.length, 0);
}
},
onServerData: (chunk) => {
record.bytesSent += chunk.length;
this.timeoutManager.updateActivity(record);
this.smartProxy.timeoutManager.updateActivity(record);
// Record bytes for metrics
if (this.smartProxy.metricsCollector) {
this.smartProxy.metricsCollector.recordBytes(record.id, 0, chunk.length);
}
},
onCleanup: (reason) => {
this.connectionManager.cleanupConnection(record, reason);
this.smartProxy.connectionManager.cleanupConnection(record, reason);
},
enableHalfOpen: false // Default: close both when one closes (required for proxy chains)
});
// Apply timeouts if keep-alive is enabled
if (record.hasKeepAlive) {
socket.setTimeout(this.settings.socketTimeout || 3600000);
targetSocket.setTimeout(this.settings.socketTimeout || 3600000);
socket.setTimeout(this.smartProxy.settings.socketTimeout || 3600000);
targetSocket.setTimeout(this.smartProxy.settings.socketTimeout || 3600000);
}
// Log successful connection
@ -1333,11 +1333,11 @@ export class RouteConnectionHandler {
};
// Create a renegotiation handler function
const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
const renegotiationHandler = this.smartProxy.tlsManager.createRenegotiationHandler(
connectionId,
serverName,
connInfo,
(_connectionId, reason) => this.connectionManager.cleanupConnection(record, reason)
(_connectionId, reason) => this.smartProxy.connectionManager.cleanupConnection(record, reason)
);
// Store the handler in the connection record so we can remove it during cleanup
@ -1346,7 +1346,7 @@ export class RouteConnectionHandler {
// Add the handler to the socket
socket.on('data', renegotiationHandler);
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('info', `TLS renegotiation handler installed for connection ${connectionId} with SNI ${serverName}`, {
connectionId,
serverName,
@ -1356,13 +1356,13 @@ export class RouteConnectionHandler {
}
// Set connection timeout
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
record.cleanupTimer = this.smartProxy.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
logger.log('warn', `Connection ${connectionId} from ${record.remoteIP} exceeded max lifetime, forcing cleanup`, {
connectionId,
remoteIP: record.remoteIP,
component: 'route-handler'
});
this.connectionManager.cleanupConnection(record, reason);
this.smartProxy.connectionManager.cleanupConnection(record, reason);
});
// Mark TLS handshake as complete for TLS connections
@ -1377,14 +1377,14 @@ export class RouteConnectionHandler {
record.outgoingStartTime = Date.now();
// Apply socket optimizations
targetSocket.setNoDelay(this.settings.noDelay);
targetSocket.setNoDelay(this.smartProxy.settings.noDelay);
// Apply keep-alive settings if enabled
if (this.settings.keepAlive) {
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
if (this.smartProxy.settings.keepAlive) {
targetSocket.setKeepAlive(true, this.smartProxy.settings.keepAliveInitialDelay);
// Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) {
if (this.smartProxy.settings.enableKeepAliveProbes) {
try {
if ('setKeepAliveProbes' in targetSocket) {
(targetSocket as any).setKeepAliveProbes(10);
@ -1394,7 +1394,7 @@ export class RouteConnectionHandler {
}
} catch (err) {
// Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) {
if (this.smartProxy.settings.enableDetailedLogging) {
logger.log('warn', `Enhanced TCP keep-alive not supported for outgoing socket on connection ${connectionId}: ${err}`, {
connectionId,
error: err,
@ -1406,16 +1406,16 @@ export class RouteConnectionHandler {
}
// Setup error handlers for incoming socket
socket.on('error', this.connectionManager.handleError('incoming', record));
socket.on('error', this.smartProxy.connectionManager.handleError('incoming', record));
// Handle timeouts with keep-alive awareness
socket.on('timeout', () => {
// For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) {
logger.log('warn', `Timeout event on incoming keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`, {
logger.log('warn', `Timeout event on incoming keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}. Connection preserved.`, {
connectionId,
remoteIP: record.remoteIP,
timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
status: 'Connection preserved',
component: 'route-handler'
});
@ -1423,26 +1423,26 @@ export class RouteConnectionHandler {
}
// For non-keep-alive connections, proceed with normal cleanup
logger.log('warn', `Timeout on incoming side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`, {
logger.log('warn', `Timeout on incoming side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}`, {
connectionId,
remoteIP: record.remoteIP,
timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
component: 'route-handler'
});
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'timeout';
this.connectionManager.incrementTerminationStat('incoming', 'timeout');
this.smartProxy.connectionManager.incrementTerminationStat('incoming', 'timeout');
}
this.connectionManager.cleanupConnection(record, 'timeout_incoming');
this.smartProxy.connectionManager.cleanupConnection(record, 'timeout_incoming');
});
targetSocket.on('timeout', () => {
// For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) {
logger.log('warn', `Timeout event on outgoing keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}. Connection preserved.`, {
logger.log('warn', `Timeout event on outgoing keep-alive connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}. Connection preserved.`, {
connectionId,
remoteIP: record.remoteIP,
timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
status: 'Connection preserved',
component: 'route-handler'
});
@ -1450,20 +1450,20 @@ export class RouteConnectionHandler {
}
// For non-keep-alive connections, proceed with normal cleanup
logger.log('warn', `Timeout on outgoing side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`, {
logger.log('warn', `Timeout on outgoing side for connection ${connectionId} from ${record.remoteIP} after ${plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000)}`, {
connectionId,
remoteIP: record.remoteIP,
timeout: plugins.prettyMs(this.settings.socketTimeout || 3600000),
timeout: plugins.prettyMs(this.smartProxy.settings.socketTimeout || 3600000),
component: 'route-handler'
});
if (record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = 'timeout';
this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
this.smartProxy.connectionManager.incrementTerminationStat('outgoing', 'timeout');
}
this.connectionManager.cleanupConnection(record, 'timeout_outgoing');
this.smartProxy.connectionManager.cleanupConnection(record, 'timeout_outgoing');
});
// Apply socket timeouts
this.timeoutManager.applySocketTimeouts(record);
this.smartProxy.timeoutManager.applySocketTimeouts(record);
}
}