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('comprehensive connection cleanup test - all scenarios', async () => { console.log('\n=== Comprehensive Connection Cleanup Test ==='); // Create a SmartProxy instance const proxy = new SmartProxy({ ports: [8570, 8571], // One for immediate routing, one for TLS enableDetailedLogging: false, initialDataTimeout: 2000, socketTimeout: 5000, routes: [ { name: 'non-tls-route', match: { ports: 8570 }, action: { type: 'forward', target: { host: 'localhost', port: 9999 // Non-existent port } } }, { name: 'tls-route', match: { ports: 8571 }, action: { type: 'forward', target: { host: 'localhost', port: 9999 // Non-existent port }, tls: { mode: 'passthrough' } } } ] }); // Start the proxy await proxy.start(); console.log('āœ“ Proxy started on ports 8570 (non-TLS) and 8571 (TLS)'); // 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}`); // Test 1: Rapid ECONNREFUSED retries (from original issue) console.log('\n--- Test 1: Rapid ECONNREFUSED retries ---'); for (let i = 0; i < 10; i++) { await new Promise((resolve) => { const client = new net.Socket(); client.on('error', () => { client.destroy(); resolve(); }); client.on('close', () => { resolve(); }); client.connect(8570, 'localhost', () => { // Send data to trigger routing client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n'); }); setTimeout(() => { if (!client.destroyed) { client.destroy(); } resolve(); }, 100); }); if ((i + 1) % 5 === 0) { const count = getActiveConnections(); console.log(`After ${i + 1} ECONNREFUSED retries: ${count} active connections`); } } // Test 2: Connect without sending data (immediate disconnect) console.log('\n--- Test 2: Connect without sending data ---'); for (let i = 0; i < 10; i++) { const client = new net.Socket(); client.on('error', () => { // Ignore }); // Connect to non-TLS port and immediately disconnect client.connect(8570, 'localhost', () => { client.destroy(); }); await new Promise(resolve => setTimeout(resolve, 10)); } const afterNoData = getActiveConnections(); console.log(`After connect-without-data test: ${afterNoData} active connections`); // Test 3: TLS connections that disconnect before handshake console.log('\n--- Test 3: TLS early disconnect ---'); for (let i = 0; i < 10; i++) { const client = new net.Socket(); client.on('error', () => { // Ignore }); // Connect to TLS port but disconnect before sending handshake client.connect(8571, 'localhost', () => { // Wait 50ms then disconnect (before initial data timeout) setTimeout(() => { client.destroy(); }, 50); }); await new Promise(resolve => setTimeout(resolve, 100)); } const afterTlsEarly = getActiveConnections(); console.log(`After TLS early disconnect test: ${afterTlsEarly} active connections`); // Test 4: Mixed pattern - simulating real-world chaos console.log('\n--- Test 4: Mixed chaos pattern ---'); const promises = []; for (let i = 0; i < 30; i++) { promises.push(new Promise((resolve) => { const client = new net.Socket(); const port = i % 2 === 0 ? 8570 : 8571; client.on('error', () => { resolve(); }); client.on('close', () => { resolve(); }); client.connect(port, 'localhost', () => { const scenario = i % 5; switch (scenario) { case 0: // Immediate disconnect client.destroy(); break; case 1: // Send data then disconnect client.write('GET / HTTP/1.1\r\nHost: test.com\r\n\r\n'); setTimeout(() => client.destroy(), 20); break; case 2: // Disconnect after delay setTimeout(() => client.destroy(), 100); break; case 3: // Send partial TLS handshake if (port === 8571) { client.write(Buffer.from([0x16, 0x03, 0x01])); // Partial TLS } setTimeout(() => client.destroy(), 50); break; case 4: // Just let it timeout break; } }); // Failsafe setTimeout(() => { if (!client.destroyed) { client.destroy(); } resolve(); }, 500); })); // Small delay between connections if (i % 5 === 0) { await new Promise(resolve => setTimeout(resolve, 10)); } } await Promise.all(promises); console.log('āœ“ Chaos test completed'); // Wait for any cleanup await new Promise(resolve => setTimeout(resolve, 1000)); const afterChaos = getActiveConnections(); console.log(`After chaos test: ${afterChaos} active connections`); // Test 5: NFTables route (should cleanup properly) console.log('\n--- Test 5: NFTables route cleanup ---'); const nftProxy = new SmartProxy({ ports: [8572], enableDetailedLogging: false, routes: [{ name: 'nftables-route', match: { ports: 8572 }, action: { type: 'forward', forwardingEngine: 'nftables', target: { host: 'localhost', port: 9999 } } }] }); await nftProxy.start(); const getNftConnections = () => { const connectionManager = (nftProxy as any).connectionManager; return connectionManager ? connectionManager.getConnectionCount() : 0; }; // Create NFTables connections for (let i = 0; i < 5; i++) { const client = new net.Socket(); client.on('error', () => { // Ignore }); client.connect(8572, 'localhost', () => { setTimeout(() => client.destroy(), 50); }); await new Promise(resolve => setTimeout(resolve, 100)); } await new Promise(resolve => setTimeout(resolve, 500)); const nftFinal = getNftConnections(); console.log(`NFTables connections after test: ${nftFinal}`); await nftProxy.stop(); // Final check on main proxy const finalCount = getActiveConnections(); console.log(`\nFinal connection count: ${finalCount}`); // Stop the proxy await proxy.stop(); console.log('āœ“ Proxy stopped'); // Verify all connections were cleaned up expect(finalCount).toEqual(initialCount); expect(afterNoData).toEqual(initialCount); expect(afterTlsEarly).toEqual(initialCount); expect(afterChaos).toEqual(initialCount); expect(nftFinal).toEqual(0); console.log('\nāœ… PASS: Comprehensive connection cleanup test passed!'); console.log('All connection scenarios properly cleaned up:'); console.log('- ECONNREFUSED rapid retries'); console.log('- Connect without sending data'); console.log('- TLS early disconnect'); console.log('- Mixed chaos patterns'); console.log('- NFTables connections'); }); tap.start();