Improve error handling and logging for outgoing connections in RouteConnectionHandler

This commit is contained in:
2025-06-01 13:58:20 +00:00
parent 705a59413d
commit 2d240671ab
2 changed files with 192 additions and 357 deletions

View File

@ -1074,19 +1074,13 @@ export class RouteConnectionHandler {
}
// Create the target socket with immediate error handling
let targetSocket: plugins.net.Socket;
let connectionEstablished = false;
// Flag to track if initial connection failed
let connectionFailed = false;
targetSocket = createSocketWithErrorHandler({
const targetSocket = createSocketWithErrorHandler({
port: finalTargetPort,
host: finalTargetHost,
onError: (error) => {
// Mark connection as failed
connectionFailed = true;
// Connection failed - clean up immediately
// Connection failed - clean up everything immediately
logger.log('error',
`Connection setup error for ${connectionId} to ${finalTargetHost}:${finalTargetPort}: ${error.message} (${(error as any).code})`,
{
@ -1099,6 +1093,20 @@ export class RouteConnectionHandler {
}
);
// Log specific error types for easier debugging
if ((error as any).code === 'ECONNREFUSED') {
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'
}
);
}
// Resume the incoming socket to prevent it from hanging
socket.resume();
@ -1107,18 +1115,184 @@ export class RouteConnectionHandler {
socket.destroy();
}
// Clean up the connection record
this.connectionManager.initiateCleanupOnce(record, `connection_failed_${(error as any).code || 'unknown'}`);
// Clean up the connection record - this is critical!
this.connectionManager.cleanupConnection(record, `connection_failed_${(error as any).code || 'unknown'}`);
},
onConnect: () => {
connectionEstablished = true;
if (this.settings.enableDetailedLogging) {
logger.log('info', `Connection ${connectionId} established to target ${finalTargetHost}:${finalTargetPort}`, {
connectionId,
targetHost: finalTargetHost,
targetPort: finalTargetPort,
component: 'route-handler'
});
}
// Clear any error listeners added by createSocketWithErrorHandler
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) {
logger.log('error', `Error writing pending data to target for connection ${connectionId}: ${err.message}`, {
connectionId,
error: err.message,
component: 'route-handler'
});
return this.connectionManager.initiateCleanupOnce(record, 'write_error');
}
});
// Clear the buffer now that we've processed it
record.pendingData = [];
record.pendingDataSize = 0;
}
// Set up independent socket handlers for half-open connection support
const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
socket,
targetSocket,
(reason) => {
this.connectionManager.initiateCleanupOnce(record, reason);
}
);
// Setup socket handlers with custom timeout handling
setupSocketHandlers(socket, cleanupClient, (sock) => {
// Don't close on timeout for keep-alive connections
if (record.hasKeepAlive) {
sock.setTimeout(this.settings.socketTimeout || 3600000);
}
}, 'client');
setupSocketHandlers(targetSocket, cleanupServer, (sock) => {
// Don't close on timeout for keep-alive connections
if (record.hasKeepAlive) {
sock.setTimeout(this.settings.socketTimeout || 3600000);
}
}, 'server');
// Forward data from client to target with backpressure handling
socket.on('data', (chunk: Buffer) => {
record.bytesReceived += chunk.length;
this.timeoutManager.updateActivity(record);
if (targetSocket.writable) {
const flushed = targetSocket.write(chunk);
// Handle backpressure
if (!flushed) {
socket.pause();
targetSocket.once('drain', () => {
socket.resume();
});
}
}
});
// Forward data from target to client with backpressure handling
targetSocket.on('data', (chunk: Buffer) => {
record.bytesSent += chunk.length;
this.timeoutManager.updateActivity(record);
if (socket.writable) {
const flushed = socket.write(chunk);
// Handle backpressure
if (!flushed) {
targetSocket.pause();
socket.once('drain', () => {
targetSocket.resume();
});
}
}
});
// Log successful connection
logger.log('info',
`Connection established: ${record.remoteIP} -> ${finalTargetHost}:${finalTargetPort}` +
`${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'
}
);
// 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) {
logger.log('info', `TLS renegotiation handler installed for connection ${connectionId} with SNI ${serverName}`, {
connectionId,
serverName,
component: 'route-handler'
});
}
}
// Set connection timeout
record.cleanupTimer = this.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.initiateCleanupOnce(record, reason);
});
// Mark TLS handshake as complete for TLS connections
if (record.isTLS) {
record.tlsHandshakeComplete = true;
}
}
});
// Only proceed with setup if connection didn't fail immediately
if (!connectionFailed) {
record.outgoing = targetSocket;
record.outgoingStartTime = Date.now();
// Only set up basic properties - everything else happens in onConnect
record.outgoing = targetSocket;
record.outgoingStartTime = Date.now();
// Apply socket optimizations
targetSocket.setNoDelay(this.settings.noDelay);
// Apply socket optimizations
targetSocket.setNoDelay(this.settings.noDelay);
// Apply keep-alive settings if enabled
if (this.settings.keepAlive) {
@ -1146,12 +1320,6 @@ export class RouteConnectionHandler {
}
}
// Setup improved error handling for outgoing connection
this.setupOutgoingErrorHandler(connectionId, targetSocket, record, socket, finalTargetHost, finalTargetPort);
// Note: Close handlers are managed by independent socket handlers above
// We don't register handleClose here to avoid bilateral cleanup
// Setup error handlers for incoming socket
socket.on('error', this.connectionManager.handleError('incoming', record));
@ -1213,178 +1381,10 @@ export class RouteConnectionHandler {
// Apply socket timeouts
this.timeoutManager.applySocketTimeouts(record);
// Track outgoing data for bytes counting
// Track outgoing data for bytes counting (moved from the duplicate connect handler)
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) {
logger.log('info', `Connection ${connectionId} established to target ${finalTargetHost}:${finalTargetPort}`, {
connectionId,
targetHost: finalTargetHost,
targetPort: finalTargetPort,
component: 'route-handler'
});
}
// 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) {
logger.log('error', `Error writing pending data to target for connection ${connectionId}: ${err.message}`, {
connectionId,
error: err.message,
component: 'route-handler'
});
return this.connectionManager.initiateCleanupOnce(record, 'write_error');
}
});
// Clear the buffer now that we've processed it
record.pendingData = [];
record.pendingDataSize = 0;
}
// Set up independent socket handlers for half-open connection support
const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
socket,
targetSocket,
(reason) => {
this.connectionManager.initiateCleanupOnce(record, reason);
}
);
// Setup socket handlers with custom timeout handling
setupSocketHandlers(socket, cleanupClient, (sock) => {
// Don't close on timeout for keep-alive connections
if (record.hasKeepAlive) {
sock.setTimeout(this.settings.socketTimeout || 3600000);
}
}, 'client');
setupSocketHandlers(targetSocket, cleanupServer, (sock) => {
// Don't close on timeout for keep-alive connections
if (record.hasKeepAlive) {
sock.setTimeout(this.settings.socketTimeout || 3600000);
}
}, 'server');
// Forward data from client to target with backpressure handling
socket.on('data', (chunk: Buffer) => {
record.bytesReceived += chunk.length;
this.timeoutManager.updateActivity(record);
if (targetSocket.writable) {
const flushed = targetSocket.write(chunk);
// Handle backpressure
if (!flushed) {
socket.pause();
targetSocket.once('drain', () => {
socket.resume();
});
}
}
});
// Forward data from target to client with backpressure handling
targetSocket.on('data', (chunk: Buffer) => {
record.bytesSent += chunk.length;
this.timeoutManager.updateActivity(record);
if (socket.writable) {
const flushed = socket.write(chunk);
// Handle backpressure
if (!flushed) {
targetSocket.pause();
socket.once('drain', () => {
targetSocket.resume();
});
}
}
});
// Log successful connection
logger.log('info',
`Connection established: ${record.remoteIP} -> ${finalTargetHost}:${finalTargetPort}` +
`${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'
}
);
// 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) {
logger.log('info', `TLS renegotiation handler installed for connection ${connectionId} with SNI ${serverName}`, {
connectionId,
serverName,
component: 'route-handler'
});
}
}
// Set connection timeout
record.cleanupTimer = this.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.initiateCleanupOnce(record, reason);
});
// Mark TLS handshake as complete for TLS connections
if (record.isTLS) {
record.tlsHandshakeComplete = true;
}
});
} // End of if (!connectionFailed)
}
}