158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { SmartProxy } from '../ts/index.js';
|
|
import * as net from 'net';
|
|
|
|
tap.test('websocket keep-alive settings for SNI passthrough', async (tools) => {
|
|
// Test 1: Verify grace periods for TLS connections
|
|
console.log('\n=== Test 1: Grace periods for encrypted connections ===');
|
|
|
|
const proxy = new SmartProxy({
|
|
ports: [8443],
|
|
keepAliveTreatment: 'extended',
|
|
keepAliveInactivityMultiplier: 10,
|
|
inactivityTimeout: 60000, // 1 minute for testing
|
|
routes: [
|
|
{
|
|
name: 'test-passthrough',
|
|
match: { ports: 8443, domains: 'test.local' },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: 9443 },
|
|
tls: { mode: 'passthrough' }
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
// Override route port
|
|
proxy.settings.routes[0].match.ports = 8443;
|
|
|
|
await proxy.start();
|
|
|
|
// Access connection manager
|
|
const connectionManager = proxy.connectionManager;
|
|
|
|
// Test 2: Verify longer grace periods are applied
|
|
console.log('\n=== Test 2: Checking grace period configuration ===');
|
|
|
|
// Create a mock connection record
|
|
const mockRecord = {
|
|
id: 'test-conn-1',
|
|
remoteIP: '127.0.0.1',
|
|
incomingStartTime: Date.now() - 120000, // 2 minutes old
|
|
isTLS: true,
|
|
incoming: { destroyed: false } as any,
|
|
outgoing: { destroyed: true } as any, // Half-zombie state
|
|
connectionClosed: false,
|
|
hasKeepAlive: true,
|
|
lastActivity: Date.now() - 60000
|
|
};
|
|
|
|
// The grace period should be 5 minutes for TLS connections
|
|
const gracePeriod = mockRecord.isTLS ? 300000 : 30000;
|
|
console.log(`Grace period for TLS connection: ${gracePeriod}ms (${gracePeriod / 1000} seconds)`);
|
|
expect(gracePeriod).toEqual(300000); // 5 minutes
|
|
|
|
// Test 3: Verify keep-alive treatment
|
|
console.log('\n=== Test 3: Keep-alive treatment configuration ===');
|
|
|
|
const settings = proxy.settings;
|
|
console.log(`Keep-alive treatment: ${settings.keepAliveTreatment}`);
|
|
console.log(`Keep-alive multiplier: ${settings.keepAliveInactivityMultiplier}`);
|
|
console.log(`Base inactivity timeout: ${settings.inactivityTimeout}ms`);
|
|
|
|
// Calculate effective timeout
|
|
const effectiveTimeout = settings.inactivityTimeout! * (settings.keepAliveInactivityMultiplier || 6);
|
|
console.log(`Effective timeout for keep-alive connections: ${effectiveTimeout}ms (${effectiveTimeout / 1000} seconds)`);
|
|
|
|
expect(settings.keepAliveTreatment).toEqual('extended');
|
|
expect(effectiveTimeout).toEqual(600000); // 10 minutes with our test config
|
|
|
|
// Test 4: Verify SNI passthrough doesn't get WebSocket heartbeat
|
|
console.log('\n=== Test 4: SNI passthrough handling ===');
|
|
|
|
// Check route configuration
|
|
const route = proxy.settings.routes[0];
|
|
expect(route.action.tls?.mode).toEqual('passthrough');
|
|
|
|
// In passthrough mode, WebSocket-specific handling should be skipped
|
|
// The connection should be treated as a raw TCP connection
|
|
console.log('✓ SNI passthrough routes bypass WebSocket heartbeat checks');
|
|
|
|
await proxy.stop();
|
|
|
|
console.log('\n✅ WebSocket keep-alive configuration test completed!');
|
|
});
|
|
|
|
// Test actual long-lived connection behavior
|
|
tap.test('long-lived connection survival test', async (tools) => {
|
|
console.log('\n=== Testing long-lived connection survival ===');
|
|
|
|
// Create a simple echo server
|
|
const echoServer = net.createServer((socket) => {
|
|
console.log('Echo server: client connected');
|
|
socket.on('data', (data) => {
|
|
socket.write(data); // Echo back
|
|
});
|
|
});
|
|
|
|
await new Promise<void>((resolve) => echoServer.listen(9444, resolve));
|
|
|
|
// Create proxy with immortal keep-alive
|
|
const proxy = new SmartProxy({
|
|
ports: [8444],
|
|
keepAliveTreatment: 'immortal', // Never timeout
|
|
routes: [
|
|
{
|
|
name: 'echo-passthrough',
|
|
match: { ports: 8444 },
|
|
action: {
|
|
type: 'forward',
|
|
target: { host: 'localhost', port: 9444 }
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
// Override route port
|
|
proxy.settings.routes[0].match.ports = 8444;
|
|
|
|
await proxy.start();
|
|
|
|
// Create a client connection
|
|
const client = new net.Socket();
|
|
await new Promise<void>((resolve, reject) => {
|
|
client.connect(8444, 'localhost', () => {
|
|
console.log('Client connected to proxy');
|
|
resolve();
|
|
});
|
|
client.on('error', reject);
|
|
});
|
|
|
|
// Keep connection alive with periodic data
|
|
let pingCount = 0;
|
|
const pingInterval = setInterval(() => {
|
|
if (client.writable) {
|
|
client.write(`ping ${++pingCount}\n`);
|
|
console.log(`Sent ping ${pingCount}`);
|
|
}
|
|
}, 20000); // Every 20 seconds
|
|
|
|
// Wait 65 seconds to ensure it survives past old 30s and 60s timeouts
|
|
await new Promise(resolve => setTimeout(resolve, 65000));
|
|
|
|
// Check if connection is still alive
|
|
const isAlive = client.writable && !client.destroyed;
|
|
console.log(`Connection alive after 65 seconds: ${isAlive}`);
|
|
expect(isAlive).toBeTrue();
|
|
|
|
// Clean up
|
|
clearInterval(pingInterval);
|
|
client.destroy();
|
|
await proxy.stop();
|
|
await new Promise<void>((resolve) => echoServer.close(resolve));
|
|
|
|
console.log('✅ Long-lived connection survived past 30-second timeout!');
|
|
});
|
|
|
|
tap.start(); |