- Implement immediate cleanup for connection failures to prevent leaks - Add NFTables cleanup on socket close to manage memory usage - Fix connection limit bypass by checking record after creation - Introduce tests for rapid connection retries and routing failures
201 lines
5.9 KiB
TypeScript
201 lines
5.9 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import * as plugins from '../ts/plugins.js';
|
|
|
|
// Import SmartProxy and configurations
|
|
import { SmartProxy } from '../ts/index.js';
|
|
|
|
tap.test('should handle rapid connection retries without leaking connections', async () => {
|
|
console.log('\n=== Testing Rapid Connection Retry Cleanup ===');
|
|
|
|
// Create a SmartProxy instance
|
|
const proxy = new SmartProxy({
|
|
ports: [8550],
|
|
enableDetailedLogging: false,
|
|
maxConnectionLifetime: 10000,
|
|
socketTimeout: 5000,
|
|
routes: [{
|
|
name: 'test-route',
|
|
match: { ports: 8550 },
|
|
action: {
|
|
type: 'forward',
|
|
target: {
|
|
host: 'localhost',
|
|
port: 9999 // Non-existent port to force connection failures
|
|
}
|
|
}
|
|
}]
|
|
});
|
|
|
|
// Start the proxy
|
|
await proxy.start();
|
|
console.log('✓ Proxy started on port 8550');
|
|
|
|
// Helper to get active connection count
|
|
const getActiveConnections = () => {
|
|
const connectionManager = (proxy as any).connectionManager;
|
|
return connectionManager ? connectionManager.getConnectionCount() : 0;
|
|
};
|
|
|
|
// Track connection counts
|
|
const connectionCounts: number[] = [];
|
|
const initialCount = getActiveConnections();
|
|
console.log(`Initial connection count: ${initialCount}`);
|
|
|
|
// Simulate rapid retries
|
|
const retryCount = 20;
|
|
const retryDelay = 50; // 50ms between retries
|
|
let successfulConnections = 0;
|
|
let failedConnections = 0;
|
|
|
|
console.log(`\nSimulating ${retryCount} rapid connection attempts...`);
|
|
|
|
for (let i = 0; i < retryCount; i++) {
|
|
await new Promise<void>((resolve) => {
|
|
const client = new net.Socket();
|
|
|
|
client.on('error', () => {
|
|
failedConnections++;
|
|
client.destroy();
|
|
resolve();
|
|
});
|
|
|
|
client.on('close', () => {
|
|
resolve();
|
|
});
|
|
|
|
client.connect(8550, 'localhost', () => {
|
|
// Send some data to trigger routing
|
|
client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n');
|
|
successfulConnections++;
|
|
});
|
|
|
|
// Force close after a short time
|
|
setTimeout(() => {
|
|
if (!client.destroyed) {
|
|
client.destroy();
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
// Small delay between retries
|
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
|
|
// Check connection count after each attempt
|
|
const currentCount = getActiveConnections();
|
|
connectionCounts.push(currentCount);
|
|
|
|
if ((i + 1) % 5 === 0) {
|
|
console.log(`After ${i + 1} attempts: ${currentCount} active connections`);
|
|
}
|
|
}
|
|
|
|
console.log(`\nConnection attempts complete:`);
|
|
console.log(`- Successful: ${successfulConnections}`);
|
|
console.log(`- Failed: ${failedConnections}`);
|
|
|
|
// Wait a bit for any pending cleanups
|
|
console.log('\nWaiting for cleanup...');
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Check final connection count
|
|
const finalCount = getActiveConnections();
|
|
console.log(`\nFinal connection count: ${finalCount}`);
|
|
|
|
// Analyze connection count trend
|
|
const maxCount = Math.max(...connectionCounts);
|
|
const avgCount = connectionCounts.reduce((a, b) => a + b, 0) / connectionCounts.length;
|
|
|
|
console.log(`\nConnection count statistics:`);
|
|
console.log(`- Maximum: ${maxCount}`);
|
|
console.log(`- Average: ${avgCount.toFixed(2)}`);
|
|
console.log(`- Initial: ${initialCount}`);
|
|
console.log(`- Final: ${finalCount}`);
|
|
|
|
// Stop the proxy
|
|
await proxy.stop();
|
|
console.log('\n✓ Proxy stopped');
|
|
|
|
// Verify results
|
|
expect(finalCount).toEqual(initialCount);
|
|
expect(maxCount).toBeLessThan(10); // Should not accumulate many connections
|
|
|
|
console.log('\n✅ PASS: Connection cleanup working correctly under rapid retries!');
|
|
});
|
|
|
|
tap.test('should handle routing failures without leaking connections', async () => {
|
|
console.log('\n=== Testing Routing Failure Cleanup ===');
|
|
|
|
// Create a SmartProxy instance with no routes
|
|
const proxy = new SmartProxy({
|
|
ports: [8551],
|
|
enableDetailedLogging: false,
|
|
maxConnectionLifetime: 10000,
|
|
socketTimeout: 5000,
|
|
routes: [] // No routes - all connections will fail routing
|
|
});
|
|
|
|
// Start the proxy
|
|
await proxy.start();
|
|
console.log('✓ Proxy started on port 8551 with no routes');
|
|
|
|
// Helper to get active connection count
|
|
const getActiveConnections = () => {
|
|
const connectionManager = (proxy as any).connectionManager;
|
|
return connectionManager ? connectionManager.getConnectionCount() : 0;
|
|
};
|
|
|
|
const initialCount = getActiveConnections();
|
|
console.log(`Initial connection count: ${initialCount}`);
|
|
|
|
// Create multiple connections that will fail routing
|
|
const connectionPromises = [];
|
|
for (let i = 0; i < 10; i++) {
|
|
connectionPromises.push(new Promise<void>((resolve) => {
|
|
const client = new net.Socket();
|
|
|
|
client.on('error', () => {
|
|
client.destroy();
|
|
resolve();
|
|
});
|
|
|
|
client.on('close', () => {
|
|
resolve();
|
|
});
|
|
|
|
client.connect(8551, 'localhost', () => {
|
|
// Send data to trigger routing (which will fail)
|
|
client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n');
|
|
});
|
|
|
|
// Force close after a short time
|
|
setTimeout(() => {
|
|
if (!client.destroyed) {
|
|
client.destroy();
|
|
}
|
|
resolve();
|
|
}, 500);
|
|
}));
|
|
}
|
|
|
|
// Wait for all connections to complete
|
|
await Promise.all(connectionPromises);
|
|
console.log('✓ All connection attempts completed');
|
|
|
|
// Wait for cleanup
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
|
|
const finalCount = getActiveConnections();
|
|
console.log(`Final connection count: ${finalCount}`);
|
|
|
|
// Stop the proxy
|
|
await proxy.stop();
|
|
console.log('✓ Proxy stopped');
|
|
|
|
// Verify no connections leaked
|
|
expect(finalCount).toEqual(initialCount);
|
|
|
|
console.log('\n✅ PASS: Routing failures cleaned up correctly!');
|
|
});
|
|
|
|
tap.start(); |