279 lines
7.7 KiB
TypeScript
279 lines
7.7 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('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<void>((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<void>((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(); |