smartproxy/ts/proxies/smart-proxy/route-connection-handler.ts

1013 lines
35 KiB
TypeScript

import * as plugins from '../../plugins.js';
import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
// Route checking functions have been removed
import type { IRouteConfig, IRouteAction, IRouteContext } from './models/route-types.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 { RouteManager } from './route-manager.js';
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js';
import { RedirectHandler, StaticHandler } from '../http-proxy/handlers/index.js';
/**
* Handles new connection processing and setup logic with support for route-based configuration
*/
export class RouteConnectionHandler {
private settings: ISmartProxyOptions;
// Cache for route contexts to avoid recreation
private routeContextCache: Map<string, IRouteContext> = new Map();
constructor(
settings: ISmartProxyOptions,
private connectionManager: ConnectionManager,
private securityManager: SecurityManager,
private tlsManager: TlsManager,
private httpProxyBridge: HttpProxyBridge,
private timeoutManager: TimeoutManager,
private routeManager: RouteManager
) {
this.settings = settings;
}
/**
* Create a route context object for port and host mapping functions
*/
private createRouteContext(options: {
connectionId: string;
port: number;
domain?: string;
clientIp: string;
serverIp: string;
isTls: boolean;
tlsVersion?: string;
routeName?: string;
routeId?: string;
path?: string;
query?: string;
headers?: Record<string, string>;
}): IRouteContext {
return {
// Connection information
port: options.port,
domain: options.domain,
clientIp: options.clientIp,
serverIp: options.serverIp,
path: options.path,
query: options.query,
headers: options.headers,
// TLS information
isTls: options.isTls,
tlsVersion: options.tlsVersion,
// Route information
routeName: options.routeName,
routeId: options.routeId,
// Additional properties
timestamp: Date.now(),
connectionId: options.connectionId,
};
}
/**
* Handle a new incoming connection
*/
public handleConnection(socket: plugins.net.Socket): void {
const remoteIP = socket.remoteAddress || '';
const localPort = socket.localPort || 0;
// Validate IP against rate limits and connection limits
const ipValidation = this.securityManager.validateIP(remoteIP);
if (!ipValidation.allowed) {
console.log(`Connection rejected from ${remoteIP}: ${ipValidation.reason}`);
socket.end();
socket.destroy();
return;
}
// Create a new connection record
const record = this.connectionManager.createConnection(socket);
const connectionId = record.id;
// Apply socket optimizations
socket.setNoDelay(this.settings.noDelay);
// Apply keep-alive settings if enabled
if (this.settings.keepAlive) {
socket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
record.hasKeepAlive = true;
// Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) {
try {
// These are platform-specific and may not be available
if ('setKeepAliveProbes' in socket) {
(socket as any).setKeepAliveProbes(10);
}
if ('setKeepAliveInterval' in socket) {
(socket as any).setKeepAliveInterval(1000);
}
} catch (err) {
// Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Enhanced TCP keep-alive settings not supported: ${err}`);
}
}
}
}
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] New connection from ${remoteIP} on port ${localPort}. ` +
`Keep-Alive: ${record.hasKeepAlive ? 'Enabled' : 'Disabled'}. ` +
`Active connections: ${this.connectionManager.getConnectionCount()}`
);
} else {
console.log(
`New connection from ${remoteIP} on port ${localPort}. Active connections: ${this.connectionManager.getConnectionCount()}`
);
}
// Start TLS SNI handling
this.handleTlsConnection(socket, record);
}
/**
* Handle a connection and wait for TLS handshake for SNI extraction if needed
*/
private handleTlsConnection(socket: plugins.net.Socket, record: IConnectionRecord): void {
const connectionId = record.id;
const localPort = record.localPort;
let initialDataReceived = false;
// Set an initial timeout for handshake data
let initialTimeout: NodeJS.Timeout | null = setTimeout(() => {
if (!initialDataReceived) {
console.log(
`[${connectionId}] Initial data warning (${this.settings.initialDataTimeout}ms) for connection from ${record.remoteIP}`
);
// Add a grace period
setTimeout(() => {
if (!initialDataReceived) {
console.log(`[${connectionId}] Final initial data timeout after grace period`);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'initial_timeout';
this.connectionManager.incrementTerminationStat('incoming', 'initial_timeout');
}
socket.end();
this.connectionManager.cleanupConnection(record, 'initial_timeout');
}
}, 30000);
}
}, this.settings.initialDataTimeout!);
// Make sure timeout doesn't keep the process alive
if (initialTimeout.unref) {
initialTimeout.unref();
}
// Set up error handler
socket.on('error', this.connectionManager.handleError('incoming', record));
// First data handler to capture initial TLS handshake
socket.once('data', (chunk: Buffer) => {
// Clear the initial timeout since we've received data
if (initialTimeout) {
clearTimeout(initialTimeout);
initialTimeout = null;
}
initialDataReceived = true;
record.hasReceivedInitialData = true;
// Block non-TLS connections on port 443
if (!this.tlsManager.isTlsHandshake(chunk) && localPort === 443) {
console.log(
`[${connectionId}] Non-TLS connection detected on port 443. ` +
`Terminating connection - only TLS traffic is allowed on standard HTTPS port.`
);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'non_tls_blocked';
this.connectionManager.incrementTerminationStat('incoming', 'non_tls_blocked');
}
socket.end();
this.connectionManager.cleanupConnection(record, 'non_tls_blocked');
return;
}
// Check if this looks like a TLS handshake
let serverName = '';
if (this.tlsManager.isTlsHandshake(chunk)) {
record.isTLS = true;
// Check for ClientHello to extract SNI
if (this.tlsManager.isClientHello(chunk)) {
// Create connection info for SNI extraction
const connInfo = {
sourceIp: record.remoteIP,
sourcePort: socket.remotePort || 0,
destIp: socket.localAddress || '',
destPort: socket.localPort || 0,
};
// Extract SNI
serverName = this.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) {
console.log(`[${connectionId}] No SNI detected in TLS ClientHello; sending TLS alert.`);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'session_ticket_blocked_no_sni';
this.connectionManager.incrementTerminationStat(
'incoming',
'session_ticket_blocked_no_sni'
);
}
const alert = Buffer.from([0x15, 0x03, 0x03, 0x00, 0x02, 0x01, 0x70]);
try {
socket.cork();
socket.write(alert);
socket.uncork();
socket.end();
} catch {
socket.end();
}
this.connectionManager.cleanupConnection(record, 'session_ticket_blocked_no_sni');
return;
}
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] TLS connection with SNI: ${serverName || '(empty)'}`);
}
}
}
// Find the appropriate route for this connection
this.routeConnection(socket, record, serverName, chunk);
});
}
/**
* Route the connection based on match criteria
*/
private routeConnection(
socket: plugins.net.Socket,
record: IConnectionRecord,
serverName: string,
initialChunk?: Buffer
): void {
const connectionId = record.id;
const localPort = record.localPort;
const remoteIP = record.remoteIP;
// Find matching route
const routeMatch = this.routeManager.findMatchingRoute({
port: localPort,
domain: serverName,
clientIp: remoteIP,
path: undefined, // We don't have path info at this point
tlsVersion: undefined, // We don't extract TLS version yet
});
if (!routeMatch) {
console.log(
`[${connectionId}] No route found for ${serverName || 'connection'} on port ${localPort}`
);
// No matching route, use default/fallback handling
console.log(`[${connectionId}] Using default route handling for connection`);
// Check default security settings
const defaultSecuritySettings = this.settings.defaults?.security;
if (defaultSecuritySettings) {
if (defaultSecuritySettings.ipAllowList && defaultSecuritySettings.ipAllowList.length > 0) {
const isAllowed = this.securityManager.isIPAuthorized(
remoteIP,
defaultSecuritySettings.ipAllowList,
defaultSecuritySettings.ipBlockList || []
);
if (!isAllowed) {
console.log(`[${connectionId}] IP ${remoteIP} not in default allowed list`);
socket.end();
this.connectionManager.cleanupConnection(record, 'ip_blocked');
return;
}
}
}
// Setup direct connection with default settings
if (this.settings.defaults?.target) {
// Use defaults from configuration
const targetHost = this.settings.defaults.target.host;
const targetPort = this.settings.defaults.target.port;
return this.setupDirectConnection(
socket,
record,
serverName,
initialChunk,
undefined,
targetHost,
targetPort
);
} else {
// No default target available, terminate the connection
console.log(`[${connectionId}] No default target configured. Closing connection.`);
socket.end();
this.connectionManager.cleanupConnection(record, 'no_default_target');
return;
}
}
// A matching route was found
const route = routeMatch.route;
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Route matched: "${route.name || 'unnamed'}" for ${
serverName || 'connection'
} on port ${localPort}`
);
}
// Handle the route based on its action type
switch (route.action.type) {
case 'forward':
return this.handleForwardAction(socket, record, route, initialChunk);
case 'redirect':
return this.handleRedirectAction(socket, record, route);
case 'block':
return this.handleBlockAction(socket, record, route);
case 'static':
this.handleStaticAction(socket, record, route, initialChunk);
return;
default:
console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`);
socket.end();
this.connectionManager.cleanupConnection(record, 'unknown_action');
}
}
/**
* Handle a forward action for a route
*/
private handleForwardAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig,
initialChunk?: Buffer
): void {
const connectionId = record.id;
const action = route.action as IRouteAction;
// Check if this route uses NFTables for forwarding
if (action.forwardingEngine === 'nftables') {
// NFTables handles packet forwarding at the kernel level
// The application should NOT interfere with these connections
// Log the connection for monitoring purposes
if (this.settings.enableDetailedLogging) {
console.log(
`[${record.id}] NFTables forwarding (kernel-level): ` +
`${record.remoteIP}:${socket.remotePort} -> ${socket.localAddress}:${record.localPort}` +
` (Route: "${route.name || 'unnamed'}", Domain: ${record.lockedDomain || 'n/a'})`
);
} else {
console.log(
`[${record.id}] NFTables forwarding: ${record.remoteIP} -> port ${
record.localPort
} (Route: "${route.name || 'unnamed'}")`
);
}
// Additional NFTables-specific logging if configured
if (action.nftables) {
const nftConfig = action.nftables;
if (this.settings.enableDetailedLogging) {
console.log(
`[${record.id}] NFTables config: ` +
`protocol=${nftConfig.protocol || 'tcp'}, ` +
`preserveSourceIP=${nftConfig.preserveSourceIP || false}, ` +
`priority=${nftConfig.priority || 'default'}, ` +
`maxRate=${nftConfig.maxRate || 'unlimited'}`
);
}
}
// For NFTables routes, we should still track the connection but not interfere
// Mark the connection as using network proxy so it's cleaned up properly
record.usingNetworkProxy = true;
// We don't close the socket - just let it remain open
// The kernel-level NFTables rules will handle the actual forwarding
return;
}
// We should have a target configuration for forwarding
if (!action.target) {
console.log(`[${connectionId}] Forward action missing target configuration`);
socket.end();
this.connectionManager.cleanupConnection(record, 'missing_target');
return;
}
// Create the routing context for this connection
const routeContext = this.createRouteContext({
connectionId: record.id,
port: record.localPort,
domain: record.lockedDomain,
clientIp: record.remoteIP,
serverIp: socket.localAddress || '',
isTls: record.isTLS || false,
tlsVersion: record.tlsVersion,
routeName: route.name,
routeId: route.id,
});
// Cache the context for potential reuse
this.routeContextCache.set(connectionId, routeContext);
// Determine host using function or static value
let targetHost: string | string[];
if (typeof action.target.host === 'function') {
try {
targetHost = action.target.host(routeContext);
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Dynamic host resolved to: ${
Array.isArray(targetHost) ? targetHost.join(', ') : targetHost
}`
);
}
} catch (err) {
console.log(`[${connectionId}] Error in host mapping function: ${err}`);
socket.end();
this.connectionManager.cleanupConnection(record, 'host_mapping_error');
return;
}
} else {
targetHost = action.target.host;
}
// If an array of hosts, select one randomly for load balancing
const selectedHost = Array.isArray(targetHost)
? targetHost[Math.floor(Math.random() * targetHost.length)]
: targetHost;
// Determine port using function or static value
let targetPort: number;
if (typeof action.target.port === 'function') {
try {
targetPort = action.target.port(routeContext);
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Dynamic port mapping: ${record.localPort} -> ${targetPort}`
);
}
// Store the resolved target port in the context for potential future use
routeContext.targetPort = targetPort;
} catch (err) {
console.log(`[${connectionId}] Error in port mapping function: ${err}`);
socket.end();
this.connectionManager.cleanupConnection(record, 'port_mapping_error');
return;
}
} else if (action.target.port === 'preserve') {
// Use incoming port if port is 'preserve'
targetPort = record.localPort;
} else {
// Use static port from configuration
targetPort = action.target.port;
}
// Store the resolved host in the context
routeContext.targetHost = selectedHost;
// Determine if this needs TLS handling
if (action.tls) {
switch (action.tls.mode) {
case 'passthrough':
// For TLS passthrough, just forward directly
if (this.settings.enableDetailedLogging) {
console.log(`[${connectionId}] Using TLS passthrough to ${selectedHost}:${targetPort}`);
}
return this.setupDirectConnection(
socket,
record,
record.lockedDomain,
initialChunk,
undefined,
selectedHost,
targetPort
);
case 'terminate':
case 'terminate-and-reencrypt':
// For TLS termination, use HttpProxy
if (this.httpProxyBridge.getHttpProxy()) {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Using HttpProxy for TLS termination to ${action.target.host}`
);
}
// If we have an initial chunk with TLS data, start processing it
if (initialChunk && record.isTLS) {
this.httpProxyBridge.forwardToHttpProxy(
connectionId,
socket,
record,
initialChunk,
this.settings.httpProxyPort || 8443,
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
);
return;
}
// This shouldn't normally happen - we should have TLS data at this point
console.log(`[${connectionId}] TLS termination route without TLS data`);
socket.end();
this.connectionManager.cleanupConnection(record, 'tls_error');
return;
} else {
console.log(`[${connectionId}] HttpProxy not available for TLS termination`);
socket.end();
this.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);
if (isHttpProxyPort && this.httpProxyBridge.getHttpProxy()) {
// Forward non-TLS connections to HttpProxy if configured
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Using HttpProxy for non-TLS connection on port ${record.localPort}`
);
}
this.httpProxyBridge.forwardToHttpProxy(
connectionId,
socket,
record,
initialChunk,
this.settings.httpProxyPort || 8443,
(reason) => this.connectionManager.initiateCleanupOnce(record, reason)
);
return;
} else {
// Basic forwarding
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Using basic forwarding to ${action.target.host}:${action.target.port}`
);
}
// Get the appropriate host value
let targetHost: string;
if (typeof action.target.host === 'function') {
// For function-based host, use the same routeContext created earlier
const hostResult = action.target.host(routeContext);
targetHost = Array.isArray(hostResult)
? hostResult[Math.floor(Math.random() * hostResult.length)]
: hostResult;
} else {
// For static host value
targetHost = Array.isArray(action.target.host)
? action.target.host[Math.floor(Math.random() * action.target.host.length)]
: action.target.host;
}
// Determine port - either function-based, static, or preserve incoming port
let targetPort: number;
if (typeof action.target.port === 'function') {
targetPort = action.target.port(routeContext);
} else if (action.target.port === 'preserve') {
targetPort = record.localPort;
} else {
targetPort = action.target.port;
}
// Update the connection record and context with resolved values
record.targetHost = targetHost;
record.targetPort = targetPort;
return this.setupDirectConnection(
socket,
record,
record.lockedDomain,
initialChunk,
undefined,
targetHost,
targetPort
);
}
}
}
/**
* Handle a redirect action for a route
*/
private handleRedirectAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig
): void {
// For TLS connections, we can't do redirects at the TCP level
if (record.isTLS) {
console.log(`[${record.id}] Cannot redirect TLS connection at TCP level`);
socket.end();
this.connectionManager.cleanupConnection(record, 'tls_redirect_error');
return;
}
// Delegate to HttpProxy's RedirectHandler
RedirectHandler.handleRedirect(socket, route, {
connectionId: record.id,
connectionManager: this.connectionManager,
settings: this.settings
});
}
/**
* Handle a block action for a route
*/
private handleBlockAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig
): void {
const connectionId = record.id;
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Blocking connection based on route "${route.name || 'unnamed'}"`
);
}
// Simply close the connection
socket.end();
this.connectionManager.initiateCleanupOnce(record, 'route_blocked');
}
/**
* Handle a static action for a route
*/
private async handleStaticAction(
socket: plugins.net.Socket,
record: IConnectionRecord,
route: IRouteConfig,
initialChunk?: Buffer
): Promise<void> {
// Delegate to HttpProxy's StaticHandler
await StaticHandler.handleStatic(socket, route, {
connectionId: record.id,
connectionManager: this.connectionManager,
settings: this.settings
}, record, initialChunk);
}
/**
* Setup improved error handling for the outgoing connection
*/
private setupOutgoingErrorHandler(
connectionId: string,
targetSocket: plugins.net.Socket,
record: IConnectionRecord,
socket: plugins.net.Socket,
finalTargetHost: string,
finalTargetPort: number
): void {
targetSocket.once('error', (err) => {
// This handler runs only once during the initial connection phase
const code = (err as any).code;
console.log(
`[${connectionId}] Connection setup error to ${finalTargetHost}:${finalTargetPort}: ${err.message} (${code})`
);
// Resume the incoming socket to prevent it from hanging
socket.resume();
// Log specific error types for easier debugging
if (code === 'ECONNREFUSED') {
console.log(
`[${connectionId}] Target ${finalTargetHost}:${finalTargetPort} refused connection. ` +
`Check if the target service is running and listening on that port.`
);
} else if (code === 'ETIMEDOUT') {
console.log(
`[${connectionId}] Connection to ${finalTargetHost}:${finalTargetPort} timed out. ` +
`Check network conditions, firewall rules, or if the target is too far away.`
);
} else if (code === 'ECONNRESET') {
console.log(
`[${connectionId}] Connection to ${finalTargetHost}:${finalTargetPort} was reset. ` +
`The target might have closed the connection abruptly.`
);
} else if (code === 'EHOSTUNREACH') {
console.log(
`[${connectionId}] Host ${finalTargetHost} is unreachable. ` +
`Check DNS settings, network routing, or firewall rules.`
);
} else if (code === 'ENOTFOUND') {
console.log(
`[${connectionId}] DNS lookup failed for ${finalTargetHost}. ` +
`Check your DNS settings or if the hostname is correct.`
);
}
// Clear any existing error handler after connection phase
targetSocket.removeAllListeners('error');
// Re-add the normal error handler for established connections
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
if (record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = 'connection_failed';
this.connectionManager.incrementTerminationStat('outgoing', 'connection_failed');
}
// Clean up the connection
this.connectionManager.initiateCleanupOnce(record, `connection_failed_${code}`);
});
}
/**
* Sets up a direct connection to the target
*/
private setupDirectConnection(
socket: plugins.net.Socket,
record: IConnectionRecord,
serverName?: string,
initialChunk?: Buffer,
overridePort?: number,
targetHost?: string,
targetPort?: number
): void {
const connectionId = record.id;
// Determine target host and port if not provided
const finalTargetHost =
targetHost || record.targetHost || this.settings.defaults?.target?.host || 'localhost';
// Determine target port
const finalTargetPort =
targetPort ||
record.targetPort ||
(overridePort !== undefined ? overridePort : this.settings.defaults?.target?.port || 443);
// Update record with final target information
record.targetHost = finalTargetHost;
record.targetPort = finalTargetPort;
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Setting up direct connection to ${finalTargetHost}:${finalTargetPort}`
);
}
// Setup connection options
const connectionOptions: plugins.net.NetConnectOpts = {
host: finalTargetHost,
port: finalTargetPort,
};
// Preserve source IP if configured
if (this.settings.defaults?.preserveSourceIP || this.settings.preserveSourceIP) {
connectionOptions.localAddress = record.remoteIP.replace('::ffff:', '');
}
// Store initial data if provided
if (initialChunk) {
record.bytesReceived += initialChunk.length;
record.pendingData.push(Buffer.from(initialChunk));
record.pendingDataSize = initialChunk.length;
}
// Create the target socket
const targetSocket = plugins.net.connect(connectionOptions);
record.outgoing = targetSocket;
record.outgoingStartTime = Date.now();
// Apply socket optimizations
targetSocket.setNoDelay(this.settings.noDelay);
// Apply keep-alive settings if enabled
if (this.settings.keepAlive) {
targetSocket.setKeepAlive(true, this.settings.keepAliveInitialDelay);
// Apply enhanced TCP keep-alive options if enabled
if (this.settings.enableKeepAliveProbes) {
try {
if ('setKeepAliveProbes' in targetSocket) {
(targetSocket as any).setKeepAliveProbes(10);
}
if ('setKeepAliveInterval' in targetSocket) {
(targetSocket as any).setKeepAliveInterval(1000);
}
} catch (err) {
// Ignore errors - these are optional enhancements
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Enhanced TCP keep-alive not supported for outgoing socket: ${err}`
);
}
}
}
}
// Setup improved error handling for outgoing connection
this.setupOutgoingErrorHandler(connectionId, targetSocket, record, socket, finalTargetHost, finalTargetPort);
// Setup close handlers
targetSocket.on('close', this.connectionManager.handleClose('outgoing', record));
socket.on('close', this.connectionManager.handleClose('incoming', record));
// Setup error handlers for incoming socket
socket.on('error', this.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) {
console.log(
`[${connectionId}] Timeout event on incoming keep-alive connection from ${
record.remoteIP
} after ${plugins.prettyMs(
this.settings.socketTimeout || 3600000
)}. Connection preserved.`
);
return;
}
// For non-keep-alive connections, proceed with normal cleanup
console.log(
`[${connectionId}] Timeout on incoming side from ${
record.remoteIP
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
);
if (record.incomingTerminationReason === null) {
record.incomingTerminationReason = 'timeout';
this.connectionManager.incrementTerminationStat('incoming', 'timeout');
}
this.connectionManager.initiateCleanupOnce(record, 'timeout_incoming');
});
targetSocket.on('timeout', () => {
// For keep-alive connections, just log a warning instead of closing
if (record.hasKeepAlive) {
console.log(
`[${connectionId}] Timeout event on outgoing keep-alive connection from ${
record.remoteIP
} after ${plugins.prettyMs(
this.settings.socketTimeout || 3600000
)}. Connection preserved.`
);
return;
}
// For non-keep-alive connections, proceed with normal cleanup
console.log(
`[${connectionId}] Timeout on outgoing side from ${
record.remoteIP
} after ${plugins.prettyMs(this.settings.socketTimeout || 3600000)}`
);
if (record.outgoingTerminationReason === null) {
record.outgoingTerminationReason = 'timeout';
this.connectionManager.incrementTerminationStat('outgoing', 'timeout');
}
this.connectionManager.initiateCleanupOnce(record, 'timeout_outgoing');
});
// Apply socket timeouts
this.timeoutManager.applySocketTimeouts(record);
// Track outgoing data for bytes counting
targetSocket.on('data', (chunk: Buffer) => {
record.bytesSent += chunk.length;
this.timeoutManager.updateActivity(record);
});
// Wait for the outgoing connection to be ready before setting up piping
targetSocket.once('connect', () => {
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Connection established to target: ${finalTargetHost}:${finalTargetPort}`
);
}
// Clear the initial connection error handler
targetSocket.removeAllListeners('error');
// Add the normal error handler for established connections
targetSocket.on('error', this.connectionManager.handleError('outgoing', record));
// Flush any pending data to target
if (record.pendingData.length > 0) {
const combinedData = Buffer.concat(record.pendingData);
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] Forwarding ${combinedData.length} bytes of initial data to target`
);
}
// Write pending data immediately
targetSocket.write(combinedData, (err) => {
if (err) {
console.log(`[${connectionId}] Error writing pending data to target: ${err.message}`);
return this.connectionManager.initiateCleanupOnce(record, 'write_error');
}
});
// Clear the buffer now that we've processed it
record.pendingData = [];
record.pendingDataSize = 0;
}
// Immediately setup bidirectional piping - much simpler than manual data management
socket.pipe(targetSocket);
targetSocket.pipe(socket);
// Track incoming data for bytes counting - do this after piping is set up
socket.on('data', (chunk: Buffer) => {
record.bytesReceived += chunk.length;
this.timeoutManager.updateActivity(record);
});
// Log successful connection
console.log(
`Connection established: ${record.remoteIP} -> ${finalTargetHost}:${finalTargetPort}` +
`${
serverName
? ` (SNI: ${serverName})`
: record.lockedDomain
? ` (Domain: ${record.lockedDomain})`
: ''
}`
);
// Add TLS renegotiation handler if needed
if (serverName) {
// Create connection info object for the existing connection
const connInfo = {
sourceIp: record.remoteIP,
sourcePort: record.incoming.remotePort || 0,
destIp: record.incoming.localAddress || '',
destPort: record.incoming.localPort || 0,
};
// Create a renegotiation handler function
const renegotiationHandler = this.tlsManager.createRenegotiationHandler(
connectionId,
serverName,
connInfo,
(connectionId, reason) => this.connectionManager.initiateCleanupOnce(record, reason)
);
// Store the handler in the connection record so we can remove it during cleanup
record.renegotiationHandler = renegotiationHandler;
// Add the handler to the socket
socket.on('data', renegotiationHandler);
if (this.settings.enableDetailedLogging) {
console.log(
`[${connectionId}] TLS renegotiation handler installed for SNI domain: ${serverName}`
);
}
}
// Set connection timeout
record.cleanupTimer = this.timeoutManager.setupConnectionTimeout(record, (record, reason) => {
console.log(
`[${connectionId}] Connection from ${record.remoteIP} exceeded max lifetime, forcing cleanup.`
);
this.connectionManager.initiateCleanupOnce(record, reason);
});
// Mark TLS handshake as complete for TLS connections
if (record.isTLS) {
record.tlsHandshakeComplete = true;
}
});
}
}