fix(metrics): improve metrics
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user