/** * SOCKS5 proxy server — built on Node.js net module, no external deps. * * Implements: * - SOCKS5 handshake (RFC 1928) * - No-auth method (0x00) * - CONNECT command (TCP tunnel) * - Byte tracking per connection */ import * as net from "net"; import { BandwidthTracker } from "./bandwidth"; const SOCKS_VER = 0x05; const NO_AUTH = 0x00; const CMD_CONNECT = 0x01; const ATYP_IPV4 = 0x01; const ATYP_DOMAIN = 0x03; const ATYP_IPV6 = 0x04; const REP_SUCCESS = 0x00; const REP_FAIL = 0x01; const CONN_TIMEOUT_MS = 30_000; export interface Socks5ServerOptions { port: number; host?: string; onNewConnection?: () => boolean; // return false to reject (bandwidth limit) bandwidth?: BandwidthTracker; } function buildReply(rep: number, ip = "0.0.0.0", port = 0): Buffer { const parts = ip.split(".").map(Number); const buf = Buffer.alloc(10); buf[0] = SOCKS_VER; buf[1] = rep; buf[2] = 0x00; // reserved buf[3] = ATYP_IPV4; buf[4] = parts[0] ?? 0; buf[5] = parts[1] ?? 0; buf[6] = parts[2] ?? 0; buf[7] = parts[3] ?? 0; buf.writeUInt16BE(port, 8); return buf; } function handleClient( socket: net.Socket, opts: Socks5ServerOptions ): void { socket.setTimeout(CONN_TIMEOUT_MS); socket.on("timeout", () => socket.destroy()); socket.on("error", () => socket.destroy()); let state: "greeting" | "request" | "connected" = "greeting"; let buf = Buffer.alloc(0); socket.on("data", (chunk) => { buf = Buffer.concat([buf, chunk]); if (state === "greeting") { // Need at least: VER(1) + NMETHODS(1) + methods if (buf.length < 2) return; const nmethods = buf[1] ?? 0; if (buf.length < 2 + nmethods) return; const methods = buf.slice(2, 2 + nmethods); buf = buf.slice(2 + nmethods); if (!methods.includes(NO_AUTH)) { socket.write(Buffer.from([SOCKS_VER, 0xFF])); // no acceptable method socket.destroy(); return; } socket.write(Buffer.from([SOCKS_VER, NO_AUTH])); state = "request"; return; } if (state === "request") { // Minimum: VER(1) CMD(1) RSV(1) ATYP(1) + address + port(2) if (buf.length < 4) return; const cmd = buf[1]; const atyp = buf[3]; let host: string; let port: number; let consumed: number; if (atyp === ATYP_IPV4) { if (buf.length < 10) return; host = `${buf[4]}.${buf[5]}.${buf[6]}.${buf[7]}`; port = buf.readUInt16BE(8); consumed = 10; } else if (atyp === ATYP_DOMAIN) { if (buf.length < 5) return; const domainLen = buf[4] ?? 0; if (buf.length < 5 + domainLen + 2) return; host = buf.slice(5, 5 + domainLen).toString("utf8"); port = buf.readUInt16BE(5 + domainLen); consumed = 5 + domainLen + 2; } else if (atyp === ATYP_IPV6) { if (buf.length < 22) return; const parts: string[] = []; for (let i = 0; i < 8; i++) { parts.push(buf.readUInt16BE(4 + i * 2).toString(16)); } host = parts.join(":"); port = buf.readUInt16BE(20); consumed = 22; } else { socket.write(buildReply(0x08)); // address type not supported socket.destroy(); return; } buf = buf.slice(consumed); if (cmd !== CMD_CONNECT) { socket.write(buildReply(0x07)); // command not supported socket.destroy(); return; } // Check bandwidth limit before accepting if (opts.onNewConnection && !opts.onNewConnection()) { socket.write(buildReply(REP_FAIL)); socket.destroy(); return; } opts.bandwidth?.incrementRequests(); const target = net.createConnection({ host, port }, () => { socket.write(buildReply(REP_SUCCESS)); state = "connected"; // Pipe remaining buffered bytes if (buf.length > 0) { target.write(buf); buf = Buffer.alloc(0); } // Bidirectional pipe with byte counting socket.on("data", (d) => { opts.bandwidth?.addBytes(d.length); if (!target.destroyed) target.write(d); }); target.on("data", (d) => { opts.bandwidth?.addBytes(d.length); if (!socket.destroyed) socket.write(d); }); socket.on("end", () => { if (!target.destroyed) target.end(); }); target.on("end", () => { if (!socket.destroyed) socket.end(); }); }); target.setTimeout(CONN_TIMEOUT_MS); target.on("timeout", () => { target.destroy(); socket.destroy(); }); target.on("error", () => { if (!socket.destroyed) { socket.write(buildReply(REP_FAIL)); socket.destroy(); } }); } }); } export function createSocks5Server(opts: Socks5ServerOptions): net.Server { const server = net.createServer((socket) => { handleClient(socket, opts); }); server.on("error", (err) => { console.error("[SOCKS5] Server error:", err.message); }); return server; }