2025-05-10 00:01:02 +00:00
import * as plugins from '../../plugins.js' ;
2025-05-19 13:23:16 +00:00
import type { IConnectionRecord , ISmartProxyOptions } from './models/interfaces.js' ;
2025-05-19 23:37:11 +00:00
import { logger } from '../../core/utils/logger.js' ;
2025-05-15 08:56:27 +00:00
// Route checking functions have been removed
2025-05-19 13:23:16 +00:00
import type { IRouteConfig , IRouteAction , IRouteContext } from './models/route-types.js' ;
2025-05-10 00:01:02 +00:00
import { ConnectionManager } from './connection-manager.js' ;
import { SecurityManager } from './security-manager.js' ;
import { TlsManager } from './tls-manager.js' ;
2025-05-19 17:28:05 +00:00
import { HttpProxyBridge } from './http-proxy-bridge.js' ;
2025-05-10 00:01:02 +00:00
import { TimeoutManager } from './timeout-manager.js' ;
import { RouteManager } from './route-manager.js' ;
import type { ForwardingHandler } from '../../forwarding/handlers/base-handler.js' ;
/ * *
* Handles new connection processing and setup logic with support for route - based configuration
* /
export class RouteConnectionHandler {
private settings : ISmartProxyOptions ;
2025-05-13 12:48:41 +00:00
// Cache for route contexts to avoid recreation
private routeContextCache : Map < string , IRouteContext > = new Map ( ) ;
2025-05-10 00:01:02 +00:00
constructor (
settings : ISmartProxyOptions ,
private connectionManager : ConnectionManager ,
private securityManager : SecurityManager ,
private tlsManager : TlsManager ,
2025-05-19 17:28:05 +00:00
private httpProxyBridge : HttpProxyBridge ,
2025-05-10 00:01:02 +00:00
private timeoutManager : TimeoutManager ,
private routeManager : RouteManager
) {
this . settings = settings ;
}
2025-05-13 12:48:41 +00:00
/ * *
* 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 ( ) ,
2025-05-19 13:23:16 +00:00
connectionId : options.connectionId ,
2025-05-13 12:48:41 +00:00
} ;
}
2025-05-10 00:01:02 +00:00
/ * *
* 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 ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Connection rejected ` , { remoteIP , reason : ipValidation.reason , component : 'route-handler' } ) ;
2025-05-10 00:01:02 +00:00
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 ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Enhanced TCP keep-alive settings not supported ` , { connectionId , error : err , component : 'route-handler' } ) ;
2025-05-10 00:01:02 +00:00
}
}
}
}
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' ,
` New connection from ${ remoteIP } on port ${ localPort } . ` +
` Keep-Alive: ${ record . hasKeepAlive ? 'Enabled' : 'Disabled' } . ` +
` Active connections: ${ this . connectionManager . getConnectionCount ( ) } ` ,
{
connectionId ,
remoteIP ,
localPort ,
keepAlive : record.hasKeepAlive ? 'Enabled' : 'Disabled' ,
activeConnections : this.connectionManager.getConnectionCount ( ) ,
component : 'route-handler'
}
2025-05-10 00:01:02 +00:00
) ;
} else {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' ,
` New connection from ${ remoteIP } on port ${ localPort } . Active connections: ${ this . connectionManager . getConnectionCount ( ) } ` ,
{
remoteIP ,
localPort ,
activeConnections : this.connectionManager.getConnectionCount ( ) ,
component : 'route-handler'
}
2025-05-10 00:01:02 +00:00
) ;
}
2025-05-29 12:15:53 +00:00
// Handle the connection - wait for initial data to determine if it's TLS
this . handleInitialData ( socket , record ) ;
2025-05-10 00:01:02 +00:00
}
/ * *
2025-05-29 12:15:53 +00:00
* Handle initial data from a connection to determine routing
2025-05-10 00:01:02 +00:00
* /
2025-05-29 12:15:53 +00:00
private handleInitialData ( socket : plugins.net.Socket , record : IConnectionRecord ) : void {
2025-05-10 00:01:02 +00:00
const connectionId = record . id ;
const localPort = record . localPort ;
let initialDataReceived = false ;
2025-05-29 12:15:53 +00:00
// Check if any routes on this port require TLS handling
const allRoutes = this . routeManager . getAllRoutes ( ) ;
const needsTlsHandling = allRoutes . some ( route = > {
// Check if route matches this port
const matchesPort = this . routeManager . getRoutesForPort ( localPort ) . includes ( route ) ;
return matchesPort &&
route . action . type === 'forward' &&
route . action . tls &&
( route . action . tls . mode === 'terminate' ||
route . action . tls . mode === 'passthrough' ) ;
} ) ;
// If no routes require TLS handling and it's not port 443, route immediately
if ( ! needsTlsHandling && localPort !== 443 ) {
// Set up error handler
socket . on ( 'error' , this . connectionManager . handleError ( 'incoming' , record ) ) ;
// Route immediately for non-TLS connections
this . routeConnection ( socket , record , '' , undefined ) ;
return ;
}
// Otherwise, wait for initial data to check if it's TLS
2025-05-10 00:01:02 +00:00
// Set an initial timeout for handshake data
let initialTimeout : NodeJS.Timeout | null = setTimeout ( ( ) = > {
if ( ! initialDataReceived ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` No initial data received from ${ record . remoteIP } after ${ this . settings . initialDataTimeout } ms for connection ${ connectionId } ` , {
connectionId ,
timeout : this.settings.initialDataTimeout ,
remoteIP : record.remoteIP ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
// Add a grace period
setTimeout ( ( ) = > {
if ( ! initialDataReceived ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Final initial data timeout after grace period for connection ${ connectionId } ` , {
connectionId ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
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 ) {
2025-05-19 23:37:11 +00:00
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.' ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
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 ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` No SNI detected in TLS ClientHello for connection ${ connectionId } ; sending TLS alert ` , {
connectionId ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
if ( record . incomingTerminationReason === null ) {
record . incomingTerminationReason = 'session_ticket_blocked_no_sni' ;
2025-05-19 13:23:16 +00:00
this . connectionManager . incrementTerminationStat (
'incoming' ,
'session_ticket_blocked_no_sni'
) ;
2025-05-10 00:01:02 +00:00
}
const alert = Buffer . from ( [ 0x15 , 0x03 , 0x03 , 0x00 , 0x02 , 0x01 , 0x70 ] ) ;
2025-05-19 13:23:16 +00:00
try {
socket . cork ( ) ;
socket . write ( alert ) ;
socket . uncork ( ) ;
socket . end ( ) ;
} catch {
socket . end ( ) ;
2025-05-10 00:01:02 +00:00
}
this . connectionManager . cleanupConnection ( record , 'session_ticket_blocked_no_sni' ) ;
return ;
}
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` TLS connection with SNI ` , {
connectionId ,
serverName : serverName || '(empty)' ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
}
}
}
// Find the appropriate route for this connection
this . routeConnection ( socket , record , serverName , chunk ) ;
} ) ;
}
/ * *
* Route the connection based on match criteria
* /
private routeConnection (
2025-05-19 13:23:16 +00:00
socket : plugins.net.Socket ,
2025-05-10 00:01:02 +00:00
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
2025-05-19 13:23:16 +00:00
tlsVersion : undefined , // We don't extract TLS version yet
2025-05-10 00:01:02 +00:00
} ) ;
if ( ! routeMatch ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` No route found for ${ serverName || 'connection' } on port ${ localPort } (connection: ${ connectionId } ) ` , {
connectionId ,
serverName : serverName || 'connection' ,
localPort ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
2025-05-10 00:49:39 +00:00
// No matching route, use default/fallback handling
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` Using default route handling for connection ${ connectionId } ` , {
connectionId ,
component : 'route-handler'
} ) ;
2025-05-10 00:49:39 +00:00
2025-05-10 00:01:02 +00:00
// Check default security settings
const defaultSecuritySettings = this . settings . defaults ? . security ;
if ( defaultSecuritySettings ) {
2025-05-15 09:34:01 +00:00
if ( defaultSecuritySettings . ipAllowList && defaultSecuritySettings . ipAllowList . length > 0 ) {
2025-05-10 00:01:02 +00:00
const isAllowed = this . securityManager . isIPAuthorized (
remoteIP ,
2025-05-15 09:34:01 +00:00
defaultSecuritySettings . ipAllowList ,
defaultSecuritySettings . ipBlockList || [ ]
2025-05-10 00:01:02 +00:00
) ;
2025-05-10 00:49:39 +00:00
2025-05-10 00:01:02 +00:00
if ( ! isAllowed ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` IP ${ remoteIP } not in default allowed list for connection ${ connectionId } ` , {
connectionId ,
remoteIP ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
socket . end ( ) ;
this . connectionManager . cleanupConnection ( record , 'ip_blocked' ) ;
return ;
}
}
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Setup direct connection with default settings
2025-05-10 00:49:39 +00:00
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
) ;
2025-05-10 00:28:35 +00:00
} else {
// No default target available, terminate the connection
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` No default target configured for connection ${ connectionId } . Closing connection ` , {
connectionId ,
component : 'route-handler'
} ) ;
2025-05-10 00:28:35 +00:00
socket . end ( ) ;
this . connectionManager . cleanupConnection ( record , 'no_default_target' ) ;
return ;
2025-05-10 00:01:02 +00:00
}
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// A matching route was found
const route = routeMatch . route ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` Route matched ` , {
connectionId ,
routeName : route.name || 'unnamed' ,
serverName : serverName || 'connection' ,
localPort ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
}
2025-05-19 13:23:16 +00:00
2025-05-29 12:15:53 +00:00
// Apply route-specific security checks
if ( route . security ) {
// Check IP allow/block lists
if ( route . security . ipAllowList || route . security . ipBlockList ) {
const isIPAllowed = this . securityManager . isIPAuthorized (
remoteIP ,
route . security . ipAllowList || [ ] ,
route . security . ipBlockList || [ ]
) ;
if ( ! isIPAllowed ) {
logger . log ( 'warn' , ` IP ${ remoteIP } blocked by route security for route ${ route . name || 'unnamed' } (connection: ${ connectionId } ) ` , {
connectionId ,
remoteIP ,
routeName : route.name || 'unnamed' ,
component : 'route-handler'
} ) ;
socket . end ( ) ;
this . connectionManager . cleanupConnection ( record , 'route_ip_blocked' ) ;
return ;
}
}
// Check max connections per route
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 ) {
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 ,
component : 'route-handler'
} ) ;
}
}
// Check authentication requirements
if ( route . security . authentication || route . security . basicAuth || route . security . jwtAuth ) {
// Authentication checks would typically happen at the HTTP layer
// For non-HTTP connections or passthrough, we can't enforce authentication
if ( route . action . type === 'forward' && route . action . tls ? . mode !== 'terminate' ) {
logger . log ( 'warn' , ` Route ${ route . name } has authentication configured but it cannot be enforced for non-terminated connections ` , {
connectionId ,
routeName : route.name ,
tlsMode : route.action.tls?.mode || 'none' ,
component : 'route-handler'
} ) ;
}
}
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Handle the route based on its action type
switch ( route . action . type ) {
case 'forward' :
return this . handleForwardAction ( socket , record , route , initialChunk ) ;
2025-05-19 13:23:16 +00:00
2025-05-29 00:24:57 +00:00
case 'socket-handler' :
logger . log ( 'info' , ` Handling socket-handler action for route ${ route . name } ` , {
connectionId ,
routeName : route.name ,
component : 'route-handler'
} ) ;
this . handleSocketHandlerAction ( socket , record , route , initialChunk ) ;
return ;
2025-05-10 00:01:02 +00:00
default :
2025-05-19 23:37:11 +00:00
logger . log ( 'error' , ` Unknown action type ' ${ ( route . action as any ) . type } ' for connection ${ connectionId } ` , {
connectionId ,
actionType : ( route . action as any ) . type ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
socket . end ( ) ;
this . connectionManager . cleanupConnection ( record , 'unknown_action' ) ;
}
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
/ * *
* Handle a forward action for a route
* /
private handleForwardAction (
socket : plugins.net.Socket ,
record : IConnectionRecord ,
route : IRouteConfig ,
initialChunk? : Buffer
) : void {
const connectionId = record . id ;
2025-05-19 18:29:56 +00:00
const action = route . action as IRouteAction ;
2025-05-13 12:48:41 +00:00
2025-05-15 14:35:01 +00:00
// Check if this route uses NFTables for forwarding
if ( action . forwardingEngine === 'nftables' ) {
2025-05-19 17:56:48 +00:00
// NFTables handles packet forwarding at the kernel level
// The application should NOT interfere with these connections
2025-05-19 18:29:56 +00:00
// Log the connection for monitoring purposes
2025-05-15 14:35:01 +00:00
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` NFTables forwarding (kernel-level) ` , {
connectionId : record.id ,
source : ` ${ record . remoteIP } : ${ socket . remotePort } ` ,
destination : ` ${ socket . localAddress } : ${ record . localPort } ` ,
routeName : route.name || 'unnamed' ,
domain : record.lockedDomain || 'n/a' ,
component : 'route-handler'
} ) ;
2025-05-15 14:35:01 +00:00
} else {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` NFTables forwarding ` , {
connectionId : record.id ,
remoteIP : record.remoteIP ,
localPort : record.localPort ,
routeName : route.name || 'unnamed' ,
component : 'route-handler'
} ) ;
2025-05-15 14:35:01 +00:00
}
2025-05-19 13:23:16 +00:00
2025-05-15 14:35:01 +00:00
// Additional NFTables-specific logging if configured
if ( action . nftables ) {
const nftConfig = action . nftables ;
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` NFTables config ` , {
connectionId : record.id ,
protocol : nftConfig.protocol || 'tcp' ,
preserveSourceIP : nftConfig.preserveSourceIP || false ,
priority : nftConfig.priority || 'default' ,
maxRate : nftConfig.maxRate || 'unlimited' ,
component : 'route-handler'
} ) ;
2025-05-15 14:35:01 +00:00
}
}
2025-05-19 18:29:56 +00:00
// 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 ;
2025-05-15 14:35:01 +00:00
}
2025-05-10 00:01:02 +00:00
// We should have a target configuration for forwarding
if ( ! action . target ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' , ` Forward action missing target configuration for connection ${ connectionId } ` , {
connectionId ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
socket . end ( ) ;
this . connectionManager . cleanupConnection ( record , 'missing_target' ) ;
return ;
}
2025-05-13 12:48:41 +00:00
// 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 ,
2025-05-19 13:23:16 +00:00
routeId : route.id ,
2025-05-13 12:48:41 +00:00
} ) ;
// 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 ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` Dynamic host resolved to ${ Array . isArray ( targetHost ) ? targetHost . join ( ', ' ) : targetHost } for connection ${ connectionId } ` , {
connectionId ,
targetHost : Array.isArray ( targetHost ) ? targetHost . join ( ', ' ) : targetHost ,
component : 'route-handler'
} ) ;
2025-05-13 12:48:41 +00:00
}
} catch ( err ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' , ` Error in host mapping function for connection ${ connectionId } : ${ err } ` , {
connectionId ,
error : err ,
component : 'route-handler'
} ) ;
2025-05-13 12:48:41 +00:00
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 ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` Dynamic port mapping from ${ record . localPort } to ${ targetPort } for connection ${ connectionId } ` , {
connectionId ,
sourcePort : record.localPort ,
targetPort ,
component : 'route-handler'
} ) ;
2025-05-13 12:48:41 +00:00
}
// Store the resolved target port in the context for potential future use
routeContext . targetPort = targetPort ;
} catch ( err ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' , ` Error in port mapping function for connection ${ connectionId } : ${ err } ` , {
connectionId ,
error : err ,
component : 'route-handler'
} ) ;
2025-05-13 12:48:41 +00:00
socket . end ( ) ;
this . connectionManager . cleanupConnection ( record , 'port_mapping_error' ) ;
return ;
}
2025-05-14 18:35:06 +00:00
} else if ( action . target . port === 'preserve' ) {
// Use incoming port if port is 'preserve'
2025-05-13 12:48:41 +00:00
targetPort = record . localPort ;
} else {
// Use static port from configuration
targetPort = action . target . port ;
}
// Store the resolved host in the context
routeContext . targetHost = selectedHost ;
2025-05-10 00:01:02 +00:00
// 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 ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` Using TLS passthrough to ${ selectedHost } : ${ targetPort } for connection ${ connectionId } ` , {
connectionId ,
targetHost : selectedHost ,
targetPort ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
return this . setupDirectConnection (
socket ,
record ,
record . lockedDomain ,
initialChunk ,
undefined ,
2025-05-13 12:48:41 +00:00
selectedHost ,
2025-05-10 00:01:02 +00:00
targetPort
) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
case 'terminate' :
case 'terminate-and-reencrypt' :
2025-05-19 17:28:05 +00:00
// For TLS termination, use HttpProxy
if ( this . httpProxyBridge . getHttpProxy ( ) ) {
2025-05-10 00:01:02 +00:00
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
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 ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// If we have an initial chunk with TLS data, start processing it
if ( initialChunk && record . isTLS ) {
2025-05-19 17:28:05 +00:00
this . httpProxyBridge . forwardToHttpProxy (
2025-05-10 00:01:02 +00:00
connectionId ,
socket ,
record ,
initialChunk ,
2025-05-19 17:28:05 +00:00
this . settings . httpProxyPort || 8443 ,
2025-05-10 00:01:02 +00:00
( reason ) = > this . connectionManager . initiateCleanupOnce ( record , reason )
) ;
2025-05-18 15:38:07 +00:00
return ;
2025-05-10 00:01:02 +00:00
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// This shouldn't normally happen - we should have TLS data at this point
2025-05-19 23:37:11 +00:00
logger . log ( 'error' , ` TLS termination route without TLS data for connection ${ connectionId } ` , {
connectionId ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
socket . end ( ) ;
this . connectionManager . cleanupConnection ( record , 'tls_error' ) ;
return ;
} else {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' , ` HttpProxy not available for TLS termination for connection ${ connectionId } ` , {
connectionId ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
socket . end ( ) ;
2025-05-19 17:28:05 +00:00
this . connectionManager . cleanupConnection ( record , 'no_http_proxy' ) ;
2025-05-10 00:01:02 +00:00
return ;
}
}
} else {
2025-05-19 19:59:22 +00:00
// No TLS settings - check if this port should use HttpProxy
const isHttpProxyPort = this . settings . useHttpProxy ? . includes ( record . localPort ) ;
2025-05-29 13:24:27 +00:00
// 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 ( ) } ` , {
connectionId ,
localPort : record.localPort ,
useHttpProxy : this.settings.useHttpProxy ,
isHttpProxyPort ,
hasHttpProxy : ! ! this . httpProxyBridge . getHttpProxy ( ) ,
component : 'route-handler'
} ) ;
}
2025-05-19 19:59:22 +00:00
if ( isHttpProxyPort && this . httpProxyBridge . getHttpProxy ( ) ) {
// Forward non-TLS connections to HttpProxy if configured
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` Using HttpProxy for non-TLS connection ${ connectionId } on port ${ record . localPort } ` , {
connectionId ,
port : record.localPort ,
component : 'route-handler'
} ) ;
2025-05-19 19:59:22 +00:00
}
this . httpProxyBridge . forwardToHttpProxy (
connectionId ,
socket ,
record ,
initialChunk ,
this . settings . httpProxyPort || 8443 ,
( reason ) = > this . connectionManager . initiateCleanupOnce ( record , reason )
2025-05-19 13:23:16 +00:00
) ;
2025-05-19 19:59:22 +00:00
return ;
} else {
// Basic forwarding
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
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 ,
targetPort : action.target.port ,
component : 'route-handler'
} ) ;
2025-05-19 19:59:22 +00:00
}
2025-05-19 13:23:16 +00:00
2025-05-19 19:59:22 +00:00
// 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 ;
}
2025-05-13 12:48:41 +00:00
2025-05-19 19:59:22 +00:00
// 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 ;
}
2025-05-13 12:48:41 +00:00
2025-05-19 19:59:22 +00:00
// Update the connection record and context with resolved values
record . targetHost = targetHost ;
record . targetPort = targetPort ;
2025-05-13 12:48:41 +00:00
2025-05-19 19:59:22 +00:00
return this . setupDirectConnection (
socket ,
record ,
record . lockedDomain ,
initialChunk ,
undefined ,
targetHost ,
targetPort
) ;
}
2025-05-10 00:01:02 +00:00
}
}
2025-05-19 13:23:16 +00:00
2025-05-29 00:24:57 +00:00
/ * *
* Handle a socket - handler action for a route
* /
private async handleSocketHandlerAction (
socket : plugins.net.Socket ,
record : IConnectionRecord ,
route : IRouteConfig ,
initialChunk? : Buffer
) : Promise < void > {
const connectionId = record . id ;
if ( ! route . action . socketHandler ) {
logger . log ( 'error' , 'socket-handler action missing socketHandler function' , {
connectionId ,
routeName : route.name ,
component : 'route-handler'
} ) ;
socket . destroy ( ) ;
this . connectionManager . cleanupConnection ( record , 'missing_handler' ) ;
return ;
}
2025-05-29 01:00:20 +00:00
// Create route context for the handler
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 ,
} ) ;
2025-05-29 00:24:57 +00:00
try {
2025-05-29 01:00:20 +00:00
// Call the handler with socket AND context
const result = route . action . socketHandler ( socket , routeContext ) ;
2025-05-29 00:24:57 +00:00
// Handle async handlers properly
if ( result instanceof Promise ) {
result
. then ( ( ) = > {
// Emit initial chunk after async handler completes
if ( initialChunk && initialChunk . length > 0 ) {
socket . emit ( 'data' , initialChunk ) ;
}
} )
. catch ( error = > {
logger . log ( 'error' , 'Socket handler error' , {
connectionId ,
routeName : route.name ,
error : error.message ,
component : 'route-handler'
} ) ;
if ( ! socket . destroyed ) {
socket . destroy ( ) ;
}
this . connectionManager . cleanupConnection ( record , 'handler_error' ) ;
} ) ;
} else {
// For sync handlers, emit on next tick
if ( initialChunk && initialChunk . length > 0 ) {
process . nextTick ( ( ) = > {
socket . emit ( 'data' , initialChunk ) ;
} ) ;
}
}
} catch ( error ) {
logger . log ( 'error' , 'Socket handler error' , {
connectionId ,
routeName : route.name ,
error : error.message ,
component : 'route-handler'
} ) ;
if ( ! socket . destroyed ) {
socket . destroy ( ) ;
}
this . connectionManager . cleanupConnection ( record , 'handler_error' ) ;
}
}
2025-05-19 18:29:56 +00:00
/ * *
* 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 ;
2025-05-19 23:37:11 +00:00
logger . log ( 'error' ,
` Connection setup error for ${ connectionId } to ${ finalTargetHost } : ${ finalTargetPort } : ${ err . message } ( ${ code } ) ` ,
{
connectionId ,
targetHost : finalTargetHost ,
targetPort : finalTargetPort ,
errorMessage : err.message ,
errorCode : code ,
component : 'route-handler'
}
2025-05-19 18:29:56 +00:00
) ;
// Resume the incoming socket to prevent it from hanging
socket . resume ( ) ;
// Log specific error types for easier debugging
if ( code === 'ECONNREFUSED' ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' ,
` Connection ${ connectionId } : Target ${ finalTargetHost } : ${ finalTargetPort } refused connection. Check if the target service is running and listening on that port. ` ,
{
connectionId ,
targetHost : finalTargetHost ,
targetPort : finalTargetPort ,
recommendation : 'Check if the target service is running and listening on that port.' ,
component : 'route-handler'
}
2025-05-19 18:29:56 +00:00
) ;
} else if ( code === 'ETIMEDOUT' ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' ,
` Connection ${ connectionId } to ${ finalTargetHost } : ${ finalTargetPort } timed out. Check network conditions, firewall rules, or if the target is too far away. ` ,
{
connectionId ,
targetHost : finalTargetHost ,
targetPort : finalTargetPort ,
recommendation : 'Check network conditions, firewall rules, or if the target is too far away.' ,
component : 'route-handler'
}
2025-05-19 18:29:56 +00:00
) ;
} else if ( code === 'ECONNRESET' ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' ,
` Connection ${ connectionId } to ${ finalTargetHost } : ${ finalTargetPort } was reset. The target might have closed the connection abruptly. ` ,
{
connectionId ,
targetHost : finalTargetHost ,
targetPort : finalTargetPort ,
recommendation : 'The target might have closed the connection abruptly.' ,
component : 'route-handler'
}
2025-05-19 18:29:56 +00:00
) ;
} else if ( code === 'EHOSTUNREACH' ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' ,
` Connection ${ connectionId } : Host ${ finalTargetHost } is unreachable. Check DNS settings, network routing, or firewall rules. ` ,
{
connectionId ,
targetHost : finalTargetHost ,
recommendation : 'Check DNS settings, network routing, or firewall rules.' ,
component : 'route-handler'
}
2025-05-19 18:29:56 +00:00
) ;
} else if ( code === 'ENOTFOUND' ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' ,
` Connection ${ connectionId } : DNS lookup failed for ${ finalTargetHost } . Check your DNS settings or if the hostname is correct. ` ,
{
connectionId ,
targetHost : finalTargetHost ,
recommendation : 'Check your DNS settings or if the hostname is correct.' ,
component : 'route-handler'
}
2025-05-19 18:29:56 +00:00
) ;
}
// 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 } ` ) ;
} ) ;
}
2025-05-10 00:01:02 +00:00
/ * *
* 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 ;
2025-05-10 00:49:39 +00:00
2025-05-10 00:01:02 +00:00
// Determine target host and port if not provided
2025-05-19 13:23:16 +00:00
const finalTargetHost =
targetHost || record . targetHost || this . settings . defaults ? . target ? . host || 'localhost' ;
2025-05-10 00:49:39 +00:00
// Determine target port
2025-05-19 13:23:16 +00:00
const finalTargetPort =
targetPort ||
2025-05-13 12:48:41 +00:00
record . targetPort ||
2025-05-19 13:23:16 +00:00
( overridePort !== undefined ? overridePort : this.settings.defaults?.target?.port || 443 ) ;
2025-05-10 00:01:02 +00:00
2025-05-13 12:48:41 +00:00
// Update record with final target information
record . targetHost = finalTargetHost ;
record . targetPort = finalTargetPort ;
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` Setting up direct connection ${ connectionId } to ${ finalTargetHost } : ${ finalTargetPort } ` , {
connectionId ,
targetHost : finalTargetHost ,
targetPort : finalTargetPort ,
component : 'route-handler'
} ) ;
2025-05-13 12:48:41 +00:00
}
2025-05-10 00:01:02 +00:00
// 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:' , '' ) ;
}
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Store initial data if provided
2025-05-10 00:01:02 +00:00
if ( initialChunk ) {
record . bytesReceived += initialChunk . length ;
record . pendingData . push ( Buffer . from ( initialChunk ) ) ;
record . pendingDataSize = initialChunk . length ;
}
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Create the target socket
2025-05-10 00:01:02 +00:00
const targetSocket = plugins . net . connect ( connectionOptions ) ;
record . outgoing = targetSocket ;
record . outgoingStartTime = Date . now ( ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Apply socket optimizations
targetSocket . setNoDelay ( this . settings . noDelay ) ;
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Apply keep-alive settings if enabled
2025-05-10 00:01:02 +00:00
if ( this . settings . keepAlive ) {
targetSocket . setKeepAlive ( true , this . settings . keepAliveInitialDelay ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// 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 ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Enhanced TCP keep-alive not supported for outgoing socket on connection ${ connectionId } : ${ err } ` , {
connectionId ,
error : err ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
}
}
}
}
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Setup improved error handling for outgoing connection
this . setupOutgoingErrorHandler ( connectionId , targetSocket , record , socket , finalTargetHost , finalTargetPort ) ;
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Setup close handlers
2025-05-10 00:01:02 +00:00
targetSocket . on ( 'close' , this . connectionManager . handleClose ( 'outgoing' , record ) ) ;
socket . on ( 'close' , this . connectionManager . handleClose ( 'incoming' , record ) ) ;
2025-05-19 18:29:56 +00:00
// Setup error handlers for incoming socket
socket . on ( 'error' , this . connectionManager . handleError ( 'incoming' , record ) ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Handle timeouts with keep-alive awareness
socket . on ( 'timeout' , ( ) = > {
// For keep-alive connections, just log a warning instead of closing
if ( record . hasKeepAlive ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Timeout event on incoming keep-alive connection ${ connectionId } from ${ record . remoteIP } after ${ plugins . prettyMs ( this . settings . socketTimeout || 3600000 ) } . Connection preserved. ` , {
connectionId ,
remoteIP : record.remoteIP ,
timeout : plugins.prettyMs ( this . settings . socketTimeout || 3600000 ) ,
status : 'Connection preserved' ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
return ;
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// For non-keep-alive connections, proceed with normal cleanup
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Timeout on incoming side for connection ${ connectionId } from ${ record . remoteIP } after ${ plugins . prettyMs ( this . settings . socketTimeout || 3600000 ) } ` , {
connectionId ,
remoteIP : record.remoteIP ,
timeout : plugins.prettyMs ( this . settings . socketTimeout || 3600000 ) ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
if ( record . incomingTerminationReason === null ) {
record . incomingTerminationReason = 'timeout' ;
this . connectionManager . incrementTerminationStat ( 'incoming' , 'timeout' ) ;
}
this . connectionManager . initiateCleanupOnce ( record , 'timeout_incoming' ) ;
} ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
targetSocket . on ( 'timeout' , ( ) = > {
// For keep-alive connections, just log a warning instead of closing
if ( record . hasKeepAlive ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Timeout event on outgoing keep-alive connection ${ connectionId } from ${ record . remoteIP } after ${ plugins . prettyMs ( this . settings . socketTimeout || 3600000 ) } . Connection preserved. ` , {
connectionId ,
remoteIP : record.remoteIP ,
timeout : plugins.prettyMs ( this . settings . socketTimeout || 3600000 ) ,
status : 'Connection preserved' ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
return ;
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// For non-keep-alive connections, proceed with normal cleanup
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Timeout on outgoing side for connection ${ connectionId } from ${ record . remoteIP } after ${ plugins . prettyMs ( this . settings . socketTimeout || 3600000 ) } ` , {
connectionId ,
remoteIP : record.remoteIP ,
timeout : plugins.prettyMs ( this . settings . socketTimeout || 3600000 ) ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
if ( record . outgoingTerminationReason === null ) {
record . outgoingTerminationReason = 'timeout' ;
this . connectionManager . incrementTerminationStat ( 'outgoing' , 'timeout' ) ;
}
this . connectionManager . initiateCleanupOnce ( record , 'timeout_outgoing' ) ;
} ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Apply socket timeouts
this . timeoutManager . applySocketTimeouts ( record ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Track outgoing data for bytes counting
targetSocket . on ( 'data' , ( chunk : Buffer ) = > {
record . bytesSent += chunk . length ;
this . timeoutManager . updateActivity ( record ) ;
} ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Wait for the outgoing connection to be ready before setting up piping
targetSocket . once ( 'connect' , ( ) = > {
2025-05-19 18:29:56 +00:00
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` Connection ${ connectionId } established to target ${ finalTargetHost } : ${ finalTargetPort } ` , {
connectionId ,
targetHost : finalTargetHost ,
targetPort : finalTargetPort ,
component : 'route-handler'
} ) ;
2025-05-19 18:29:56 +00:00
}
2025-05-10 00:01:02 +00:00
// Clear the initial connection error handler
targetSocket . removeAllListeners ( 'error' ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Add the normal error handler for established connections
targetSocket . on ( 'error' , this . connectionManager . handleError ( 'outgoing' , record ) ) ;
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Flush any pending data to target
2025-05-10 00:01:02 +00:00
if ( record . pendingData . length > 0 ) {
const combinedData = Buffer . concat ( record . pendingData ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
if ( this . settings . enableDetailedLogging ) {
console . log (
` [ ${ connectionId } ] Forwarding ${ combinedData . length } bytes of initial data to target `
) ;
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Write pending data immediately
targetSocket . write ( combinedData , ( err ) = > {
if ( err ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'error' , ` Error writing pending data to target for connection ${ connectionId } : ${ err . message } ` , {
connectionId ,
error : err.message ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
return this . connectionManager . initiateCleanupOnce ( record , 'write_error' ) ;
}
} ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Clear the buffer now that we've processed it
record . pendingData = [ ] ;
record . pendingDataSize = 0 ;
}
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Immediately setup bidirectional piping - much simpler than manual data management
2025-05-10 00:01:02 +00:00
socket . pipe ( targetSocket ) ;
targetSocket . pipe ( socket ) ;
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// 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 ) ;
} ) ;
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Log successful connection
2025-05-19 23:37:11 +00:00
logger . log ( 'info' ,
2025-05-19 18:29:56 +00:00
` Connection established: ${ record . remoteIP } -> ${ finalTargetHost } : ${ finalTargetPort } ` +
2025-05-19 23:37:11 +00:00
` ${ serverName ? ` (SNI: ${ serverName } ) ` : record . lockedDomain ? ` (Domain: ${ record . lockedDomain } ) ` : '' } ` ,
{
remoteIP : record.remoteIP ,
targetHost : finalTargetHost ,
targetPort : finalTargetPort ,
sni : serverName || undefined ,
domain : ! serverName && record . lockedDomain ? record.lockedDomain : undefined ,
component : 'route-handler'
}
2025-05-19 18:29:56 +00:00
) ;
2025-05-19 13:23:16 +00:00
2025-05-19 18:29:56 +00:00
// Add TLS renegotiation handler if needed
2025-05-10 00:01:02 +00:00
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 ,
} ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Create a renegotiation handler function
const renegotiationHandler = this . tlsManager . createRenegotiationHandler (
connectionId ,
serverName ,
connInfo ,
( connectionId , reason ) = > this . connectionManager . initiateCleanupOnce ( record , reason )
) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Store the handler in the connection record so we can remove it during cleanup
record . renegotiationHandler = renegotiationHandler ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Add the handler to the socket
socket . on ( 'data' , renegotiationHandler ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
if ( this . settings . enableDetailedLogging ) {
2025-05-19 23:37:11 +00:00
logger . log ( 'info' , ` TLS renegotiation handler installed for connection ${ connectionId } with SNI ${ serverName } ` , {
connectionId ,
serverName ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
}
}
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Set connection timeout
record . cleanupTimer = this . timeoutManager . setupConnectionTimeout ( record , ( record , reason ) = > {
2025-05-19 23:37:11 +00:00
logger . log ( 'warn' , ` Connection ${ connectionId } from ${ record . remoteIP } exceeded max lifetime, forcing cleanup ` , {
connectionId ,
remoteIP : record.remoteIP ,
component : 'route-handler'
} ) ;
2025-05-10 00:01:02 +00:00
this . connectionManager . initiateCleanupOnce ( record , reason ) ;
} ) ;
2025-05-19 13:23:16 +00:00
2025-05-10 00:01:02 +00:00
// Mark TLS handshake as complete for TLS connections
if ( record . isTLS ) {
record . tlsHandshakeComplete = true ;
}
} ) ;
}
2025-05-19 18:29:56 +00:00
}