File size: 4,079 Bytes
2b64d42 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | /**
* Minimal SOCKS5 tunnel — zero npm dependencies.
*
* Implements RFC 1928 (SOCKS5) + RFC 1929 (username/password auth).
* Returns a connected TCP socket ready for TLS wrapping or direct use.
*/
import net from 'node:net';
const SOCKS_VERSION = 0x05;
const AUTH_NONE = 0x00;
const AUTH_USERPASS = 0x02;
const AUTH_FAIL = 0xFF;
const CMD_CONNECT = 0x01;
const ATYP_DOMAIN = 0x03;
const REP_SUCCESS = 0x00;
export function isSocks(proxy) {
const t = (proxy?.type || '').toLowerCase();
return t === 'socks5' || t === 'socks' || t === 'socks5h';
}
export function createSocksTunnel(proxy, targetHost, targetPort, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
const host = proxy.host.replace(/:\d+$/, '');
const port = proxy.port || 1080;
let settled = false;
const done = (fn, val) => { if (!settled) { settled = true; fn(val); } };
const sock = net.connect(port, host, () => {
// Step 1: greeting — offer auth methods
const methods = proxy.username ? [AUTH_NONE, AUTH_USERPASS] : [AUTH_NONE];
sock.write(Buffer.from([SOCKS_VERSION, methods.length, ...methods]));
});
let phase = 'greeting';
let buf = Buffer.alloc(0);
sock.on('data', (chunk) => {
buf = Buffer.concat([buf, chunk]);
if (phase === 'greeting') {
if (buf.length < 2) return;
const ver = buf[0], method = buf[1];
buf = buf.subarray(2);
if (ver !== SOCKS_VERSION) {
sock.destroy();
return done(reject, new Error(`SOCKS5: server version ${ver} unsupported`));
}
if (method === AUTH_FAIL) {
sock.destroy();
return done(reject, new Error('SOCKS5: no acceptable auth method'));
}
if (method === AUTH_USERPASS && proxy.username) {
phase = 'auth';
const user = Buffer.from(proxy.username);
const pass = Buffer.from(proxy.password || '');
sock.write(Buffer.from([0x01, user.length, ...user, pass.length, ...pass]));
} else {
phase = 'connect';
sendConnect();
}
} else if (phase === 'auth') {
if (buf.length < 2) return;
const status = buf[1];
buf = buf.subarray(2);
if (status !== 0x00) {
sock.destroy();
return done(reject, new Error('SOCKS5: authentication failed'));
}
phase = 'connect';
sendConnect();
} else if (phase === 'connect') {
// Minimum response: ver(1) + rep(1) + rsv(1) + atyp(1) + addr + port(2)
if (buf.length < 4) return;
const rep = buf[1];
const atyp = buf[3];
let addrLen;
if (atyp === 0x01) addrLen = 4; // IPv4
else if (atyp === 0x04) addrLen = 16; // IPv6
else if (atyp === 0x03) addrLen = 1 + (buf.length > 4 ? buf[4] : 255); // domain
else addrLen = 0;
const totalLen = 4 + addrLen + 2;
if (buf.length < totalLen) return;
buf = buf.subarray(totalLen);
if (rep !== REP_SUCCESS) {
sock.destroy();
const reasons = {
0x01: 'general SOCKS failure', 0x02: 'connection not allowed',
0x03: 'network unreachable', 0x04: 'host unreachable',
0x05: 'connection refused', 0x06: 'TTL expired',
0x07: 'command not supported', 0x08: 'address type not supported',
};
return done(reject, new Error(`SOCKS5: ${reasons[rep] || `error ${rep}`}`));
}
phase = 'done';
done(resolve, sock);
}
});
function sendConnect() {
const domainBuf = Buffer.from(targetHost);
const portBuf = Buffer.alloc(2);
portBuf.writeUInt16BE(targetPort);
sock.write(Buffer.from([
SOCKS_VERSION, CMD_CONNECT, 0x00, ATYP_DOMAIN,
domainBuf.length, ...domainBuf, ...portBuf,
]));
}
sock.on('error', (err) => done(reject, new Error(`SOCKS5: ${err.message}`)));
sock.setTimeout(timeoutMs, () => { sock.destroy(); done(reject, new Error('SOCKS5: connection timeout')); });
});
}
|