Improve error handling and logging for outgoing connections in RouteConnectionHandler
This commit is contained in:
@ -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)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user