Implement proxy chain connection accumulation fix and add comprehensive tests
- Updated socket handling to prevent connection accumulation in chained proxies. - Introduced centralized bidirectional forwarding for consistent socket management. - Enhanced cleanup logic to ensure immediate closure of sockets when one closes. - Added tests to verify connection behavior under various scenarios, including backend failures and rapid reconnections.
This commit is contained in:
@ -109,7 +109,8 @@ export function createSocketCleanupHandler(
|
||||
export function createIndependentSocketHandlers(
|
||||
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||
serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||
onBothClosed: (reason: string) => void
|
||||
onBothClosed: (reason: string) => void,
|
||||
options: { enableHalfOpen?: boolean } = {}
|
||||
): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
|
||||
let clientClosed = false;
|
||||
let serverClosed = false;
|
||||
@ -127,8 +128,13 @@ export function createIndependentSocketHandlers(
|
||||
clientClosed = true;
|
||||
clientReason = reason;
|
||||
|
||||
// Allow server to continue if still active
|
||||
if (!serverClosed && serverSocket.writable) {
|
||||
// Default behavior: close both sockets when one closes (required for proxy chains)
|
||||
if (!serverClosed && !options.enableHalfOpen) {
|
||||
serverSocket.destroy();
|
||||
}
|
||||
|
||||
// Half-open support (opt-in only)
|
||||
if (!serverClosed && serverSocket.writable && options.enableHalfOpen) {
|
||||
// Half-close: stop reading from client, let server finish
|
||||
clientSocket.pause();
|
||||
clientSocket.unpipe(serverSocket);
|
||||
@ -145,8 +151,13 @@ export function createIndependentSocketHandlers(
|
||||
serverClosed = true;
|
||||
serverReason = reason;
|
||||
|
||||
// Allow client to continue if still active
|
||||
if (!clientClosed && clientSocket.writable) {
|
||||
// Default behavior: close both sockets when one closes (required for proxy chains)
|
||||
if (!clientClosed && !options.enableHalfOpen) {
|
||||
clientSocket.destroy();
|
||||
}
|
||||
|
||||
// Half-open support (opt-in only)
|
||||
if (!clientClosed && clientSocket.writable && options.enableHalfOpen) {
|
||||
// Half-close: stop reading from server, let client finish
|
||||
serverSocket.pause();
|
||||
serverSocket.unpipe(clientSocket);
|
||||
@ -194,6 +205,79 @@ export function setupSocketHandlers(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup bidirectional data forwarding between two sockets with proper cleanup
|
||||
* @param clientSocket The client/incoming socket
|
||||
* @param serverSocket The server/outgoing socket
|
||||
* @param handlers Object containing optional handlers for data and cleanup
|
||||
* @returns Cleanup functions for both sockets
|
||||
*/
|
||||
export function setupBidirectionalForwarding(
|
||||
clientSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||
serverSocket: plugins.net.Socket | plugins.tls.TLSSocket,
|
||||
handlers: {
|
||||
onClientData?: (chunk: Buffer) => void;
|
||||
onServerData?: (chunk: Buffer) => void;
|
||||
onCleanup: (reason: string) => void;
|
||||
enableHalfOpen?: boolean;
|
||||
}
|
||||
): { cleanupClient: (reason: string) => Promise<void>, cleanupServer: (reason: string) => Promise<void> } {
|
||||
// Set up cleanup handlers
|
||||
const { cleanupClient, cleanupServer } = createIndependentSocketHandlers(
|
||||
clientSocket,
|
||||
serverSocket,
|
||||
handlers.onCleanup,
|
||||
{ enableHalfOpen: handlers.enableHalfOpen }
|
||||
);
|
||||
|
||||
// Set up error and close handlers
|
||||
setupSocketHandlers(clientSocket, cleanupClient, undefined, 'client');
|
||||
setupSocketHandlers(serverSocket, cleanupServer, undefined, 'server');
|
||||
|
||||
// Set up data forwarding with backpressure handling
|
||||
clientSocket.on('data', (chunk: Buffer) => {
|
||||
if (handlers.onClientData) {
|
||||
handlers.onClientData(chunk);
|
||||
}
|
||||
|
||||
if (serverSocket.writable) {
|
||||
const flushed = serverSocket.write(chunk);
|
||||
|
||||
// Handle backpressure
|
||||
if (!flushed) {
|
||||
clientSocket.pause();
|
||||
serverSocket.once('drain', () => {
|
||||
if (!clientSocket.destroyed) {
|
||||
clientSocket.resume();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
serverSocket.on('data', (chunk: Buffer) => {
|
||||
if (handlers.onServerData) {
|
||||
handlers.onServerData(chunk);
|
||||
}
|
||||
|
||||
if (clientSocket.writable) {
|
||||
const flushed = clientSocket.write(chunk);
|
||||
|
||||
// Handle backpressure
|
||||
if (!flushed) {
|
||||
serverSocket.pause();
|
||||
clientSocket.once('drain', () => {
|
||||
if (!serverSocket.destroyed) {
|
||||
serverSocket.resume();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { cleanupClient, cleanupServer };
|
||||
}
|
||||
|
||||
/**
|
||||
* Pipe two sockets together with proper cleanup on either end
|
||||
* @param socket1 First socket
|
||||
|
Reference in New Issue
Block a user