import { expect, tap } from '@push.rocks/tapbundle'; import * as net from 'net'; import * as crypto from 'crypto'; import { RemoteIngressHub, RemoteIngressEdge } from '../ts/index.js'; // --------------------------------------------------------------------------- // Helpers (same patterns as test.quic.node.ts) // --------------------------------------------------------------------------- async function findFreePorts(count: number): Promise { const servers: net.Server[] = []; const ports: number[] = []; for (let i = 0; i < count; i++) { const server = net.createServer(); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); ports.push((server.address() as net.AddressInfo).port); servers.push(server); } await Promise.all(servers.map((s) => new Promise((resolve) => s.close(() => resolve())))); return ports; } type TrackingServer = net.Server & { destroyAll: () => void }; function startEchoServer(port: number, host: string): Promise { return new Promise((resolve, reject) => { const connections = new Set(); const server = net.createServer((socket) => { connections.add(socket); socket.on('close', () => connections.delete(socket)); let proxyHeaderParsed = false; let pendingBuf = Buffer.alloc(0); socket.on('data', (data: Buffer) => { if (!proxyHeaderParsed) { pendingBuf = Buffer.concat([pendingBuf, data]); const idx = pendingBuf.indexOf('\r\n'); if (idx !== -1) { proxyHeaderParsed = true; const remainder = pendingBuf.subarray(idx + 2); if (remainder.length > 0) socket.write(remainder); } return; } socket.write(data); }); socket.on('error', () => {}); }) as TrackingServer; server.destroyAll = () => { for (const conn of connections) conn.destroy(); connections.clear(); }; server.on('error', reject); server.listen(port, host, () => resolve(server)); }); } async function forceCloseServer(server: TrackingServer): Promise { server.destroyAll(); await new Promise((resolve) => server.close(() => resolve())); } function sendAndReceive(port: number, data: Buffer, timeoutMs = 30000): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; let totalReceived = 0; const expectedLength = data.length; let settled = false; const client = net.createConnection({ host: '127.0.0.1', port }, () => { client.write(data); client.end(); }); const timer = setTimeout(() => { if (!settled) { settled = true; client.destroy(); reject(new Error(`Timeout after ${timeoutMs}ms — received ${totalReceived}/${expectedLength} bytes`)); } }, timeoutMs); client.on('data', (chunk: Buffer) => { chunks.push(chunk); totalReceived += chunk.length; if (totalReceived >= expectedLength && !settled) { settled = true; clearTimeout(timer); client.destroy(); resolve(Buffer.concat(chunks)); } }); client.on('end', () => { if (!settled) { settled = true; clearTimeout(timer); resolve(Buffer.concat(chunks)); } }); client.on('error', (err) => { if (!settled) { settled = true; clearTimeout(timer); reject(err); } }); }); } function sha256(buf: Buffer): string { return crypto.createHash('sha256').update(buf).digest('hex'); } // --------------------------------------------------------------------------- // QUIC Long-Running Stability Test — 2 minutes // --------------------------------------------------------------------------- let hub: RemoteIngressHub; let edge: RemoteIngressEdge; let echoServer: TrackingServer; let hubPort: number; let edgePort: number; let disconnectCount = 0; tap.test('QUIC stability setup: start echo server and QUIC tunnel', async () => { [hubPort, edgePort] = await findFreePorts(2); echoServer = await startEchoServer(edgePort, '127.0.0.2'); hub = new RemoteIngressHub(); edge = new RemoteIngressEdge(); await hub.start({ tunnelPort: hubPort, targetHost: '127.0.0.2', }); await hub.updateAllowedEdges([ { id: 'test-edge', secret: 'test-secret', listenPorts: [edgePort] }, ]); const connectedPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('QUIC edge did not connect within 10s')), 10000); edge.once('tunnelConnected', () => { clearTimeout(timeout); resolve(); }); }); // Track disconnects — any disconnect during the test is a failure signal edge.on('tunnelDisconnected', () => { disconnectCount++; console.log(`[STABILITY] Unexpected tunnel disconnect #${disconnectCount}`); }); await edge.start({ hubHost: '127.0.0.1', hubPort, edgeId: 'test-edge', secret: 'test-secret', bindAddress: '127.0.0.1', transportMode: 'quic', }); await connectedPromise; await new Promise((resolve) => setTimeout(resolve, 500)); const status = await edge.getStatus(); expect(status.connected).toBeTrue(); }); tap.test('QUIC stability: tunnel stays alive for 30s with periodic echo probes', async () => { const testDurationMs = 30_000; // 30 seconds const probeIntervalMs = 5_000; // probe every 5 seconds const startTime = Date.now(); let probeCount = 0; let failedProbes = 0; while (Date.now() - startTime < testDurationMs) { probeCount++; const elapsed = Math.round((Date.now() - startTime) / 1000); // Verify edge still reports connected const status = await edge.getStatus(); if (!status.connected) { throw new Error(`Tunnel disconnected at ${elapsed}s (probe #${probeCount})`); } // Send a 4KB echo probe through the tunnel const data = crypto.randomBytes(4096); const hash = sha256(data); try { const received = await sendAndReceive(edgePort, data, 10000); if (received.length !== 4096 || sha256(received) !== hash) { failedProbes++; console.log(`[STABILITY] Probe #${probeCount} at ${elapsed}s: data mismatch`); } else { console.log(`[STABILITY] Probe #${probeCount} at ${elapsed}s: OK`); } } catch (err) { failedProbes++; console.log(`[STABILITY] Probe #${probeCount} at ${elapsed}s: FAILED — ${err}`); } // Wait for next probe interval const remaining = testDurationMs - (Date.now() - startTime); if (remaining > 0) { await new Promise((resolve) => setTimeout(resolve, Math.min(probeIntervalMs, remaining))); } } console.log(`[STABILITY] Completed: ${probeCount} probes, ${failedProbes} failures, ${disconnectCount} disconnects`); expect(failedProbes).toEqual(0); expect(disconnectCount).toEqual(0); // Final status check const finalStatus = await edge.getStatus(); expect(finalStatus.connected).toBeTrue(); }); tap.test('QUIC stability teardown', async () => { await edge.stop(); await hub.stop(); await forceCloseServer(echoServer); }); export default tap.start();