// Minimal loopback smoke test — spins up an in-process SOCKS5 server // that answers with an echo target, then connects Socks5Net to it and // verifies bytes round-trip. Meant as a regression check for the // handshake, not a real network test. // // npm run test:socks5 -w @dgb-wallet/net import * as net from 'node:net'; import { Socks5Net } from '../src/socks5.js'; const PROXY_PORT = 19050; const ECHO_PORT = 19051; // Trivial echo server on ECHO_PORT. const echo = net.createServer((sock) => { sock.on('data', (chunk) => sock.write(chunk)); }); // Minimal SOCKS5 server: NO-AUTH only, CONNECT to any host:port, // implemented against 127.0.0.1 (the only place we route in this test). const proxy = net.createServer((client) => { let stage = 'greet' as 'greet' | 'connect' | 'piped'; let buf: Buffer = Buffer.alloc(0); let outbound: net.Socket | null = null; client.on('data', (chunk) => { if (stage === 'piped') return; buf = Buffer.concat([buf, chunk]); if (stage === 'greet') { if (buf.length < 2) return; const n = buf[1]; if (buf.length < 2 + n) return; client.write(Buffer.from([0x05, 0x00])); // NO-AUTH accepted buf = buf.subarray(2 + n); stage = 'connect'; } if (stage === 'connect') { if (buf.length < 4) return; const atyp = buf[3]; let addrLen: number; let addr: string; let cursor: number; if (atyp === 0x03) { if (buf.length < 5) return; addrLen = buf[4]; if (buf.length < 5 + addrLen + 2) return; addr = buf.subarray(5, 5 + addrLen).toString('ascii'); cursor = 5 + addrLen; } else if (atyp === 0x01) { if (buf.length < 4 + 4 + 2) return; addr = Array.from(buf.subarray(4, 8)).join('.'); cursor = 8; } else { client.end(); return; } const port = buf.readUInt16BE(cursor); // Success reply — bound address 0.0.0.0:0 (Tor sends this too). client.write(Buffer.from([0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0])); buf = buf.subarray(cursor + 2); stage = 'piped'; outbound = net.connect({ host: addr === 'localhost' ? '127.0.0.1' : addr, port }); outbound.on('data', (d) => client.write(d)); client.on('data', (d) => outbound?.write(d)); outbound.on('close', () => client.end()); client.on('close', () => outbound?.end()); if (buf.length > 0) outbound.write(buf); } }); }); async function main(): Promise { await new Promise((r) => echo.listen(ECHO_PORT, '127.0.0.1', r)); await new Promise((r) => proxy.listen(PROXY_PORT, '127.0.0.1', r)); const client = new Socks5Net({ proxyHost: '127.0.0.1', proxyPort: PROXY_PORT }); const sock = await client.connect({ host: '127.0.0.1', port: ECHO_PORT }); const roundtrip = await new Promise((resolve, reject) => { let chunks = ''; sock.on('data', (d) => { chunks += d.toString('utf8'); if (chunks.length >= 5) resolve(chunks); }); sock.on('error', reject); sock.write('hello'); }); if (roundtrip.startsWith('hello')) { console.log(`✅ socks5 loopback: echoed ${JSON.stringify(roundtrip)}`); } else { console.error(`❌ unexpected response: ${JSON.stringify(roundtrip)}`); process.exitCode = 1; } sock.destroy(); proxy.close(); echo.close(); } main().catch((e) => { console.error('❌ test failed:', e); process.exit(1); });