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((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((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((resolve) => echoServer.close(resolve)); console.log('āœ… Long-lived connection survived past 30-second timeout!'); }); tap.start();