UNPKG

socks5server

Version:

A simple SOCKS 5/4/4a implementation and demo proxy

761 lines (707 loc) 21.2 kB
'use strict' const net = require('net'), DNS = require('dns'), dgram = require('dgram'), events = require('events'), ipAddress = require('ip-address'); const { pipeline } = require('stream'); const SOCKS_VERSION5 = 5, SOCKS_VERSION4 = 4; /* * Authentication methods ************************ * o X'00' NO AUTHENTICATION REQUIRED * o X'01' GSSAPI * o X'02' USERNAME/PASSWORD * o X'03' to X'7F' IANA ASSIGNED * o X'80' to X'FE' RESERVED FOR PRIVATE METHODS * o X'FF' NO ACCEPTABLE METHODS */ const AUTHENTICATION = { NOAUTH: 0x00, GSSAPI: 0x01, USERPASS: 0x02, NONE: 0xFF }; /* * o CMD * o CONNECT X'01' * o BIND X'02' * o UDP ASSOCIATE X'03' */ const REQUEST_CMD = { CONNECT: 0x01, BIND: 0x02, UDP_ASSOCIATE: 0x03 }; /* */ const SOCKS_REPLY = { SUCCEEDED: 0x00, SERVER_FAILURE: 0x01, NOT_ALLOWED: 0X02, NETWORK_UNREACHABLE: 0X03, HOST_UNREACHABLE: 0X04, CONNECTION_REFUSED: 0X05, TTL_EXPIRED: 0X06, COMMAND_NOT_SUPPORTED: 0X07, ADDR_NOT_SUPPORTED: 0X08, }; /* * o ATYP address type of following address * o IP V4 address: X'01' * o DOMAINNAME: X'03' * o IP V6 address: X'04' */ const ATYP = { IP_V4: 0x01, DNS: 0x03, IP_V6: 0x04 }; //CMD reply const _005B = Buffer.from([0x00, 0x5b]),//? _0101 = Buffer.from([0x01, 0x01]),//auth failed _0501 = Buffer.from([0x05, 0x01]),//general SOCKS server failure _0507 = Buffer.from([0x05, 0x07]),//Command not supported _0100 = Buffer.from([0x01, 0x00]);//auth succeeded const Address = { read: function (buffer, offset) {//offset : offset of ATYP in buffer if (buffer[offset] == ATYP.IP_V4) { return `${buffer[offset + 1]}.${buffer[offset + 2]}.${buffer[offset + 3]}.${buffer[offset + 4]}`; } else if (buffer[offset] == ATYP.DNS) { return buffer.toString('utf8', offset + 2, offset + 2 + buffer[offset + 1]); } else if (buffer[offset] == ATYP.IP_V6) { let h = [...buffer.slice(offset + 1, offset + 1 + 16)].map(num => num.toString(16).padStart(2, '0'));//to hex address //divide every 2 bytes into groups return `${h[0]}${h[1]}:${h[2]}${h[3]}:${h[4]}${h[5]}:${h[6]}${h[7]}:${h[8]}${h[9]}:${h[10]}${h[11]}:${h[12]}${h[13]}:${h[14]}${h[15]}`; } }, //size of byteLength in buffer sizeOf: function (buffer, offset) { if (buffer[offset] == ATYP.IP_V4) { return 4; } else if (buffer[offset] == ATYP.DNS) { return buffer[offset + 1]; } else if (buffer[offset] == ATYP.IP_V6) { return 16; } } }, Port = { read: function (buffer, offset) {//offset : offset of ATYP in buffer if (buffer[offset] == ATYP.IP_V4) { return buffer.readUInt16BE(8); } else if (buffer[offset] == ATYP.DNS) { return buffer.readUInt16BE(5 + buffer[offset + 1]); } else if (buffer[offset] == ATYP.IP_V6) { return buffer.readUInt16BE(20); } }, }; /* options: the same as net.Server options */ class socksServer extends net.Server { constructor(options) { super(options); this.enabledVersion = new Set([SOCKS_VERSION5, SOCKS_VERSION4]); this.enabledCmd = new Set([REQUEST_CMD.CONNECT, REQUEST_CMD.UDP_ASSOCIATE]); this.socks5 = { authMethodsList: new Set([AUTHENTICATION.NOAUTH]), authConf: { userpass: new Map(), }, authFunc: new Map([ [AUTHENTICATION.USERPASS, this._socks5UserPassAuth.bind(this)], [AUTHENTICATION.NOAUTH, this._socks5NoAuth.bind(this)], ]), }; this.on('connection', socket => { //socket._socksServer=this; socket.once('error', e => { this.emit('client_error', socket, e); }).once('data', chunk => { this._handshake(socket, chunk); }).once('socks_error', e => { this.emit('socks_error', socket, e); }); }); } /** *set authentication function for each method * * @param {number} method method defined in AUTHENTICATION * @param {function} func */ setSocks5AuthFunc(method, func) { if (typeof func !== 'function' || typeof method !== 'number') throw (new TypeError('Invalid arguments')); this.socks5.authFunc.set(method, func); } /** *set enabled authentication methods * * @param {Array[number]} list list of method defined in AUTHENTICATION */ setSocks5AuthMethods(list) { if (!Array.isArray(list)) throw (new TypeError('Not an Array')); this.socks5.authMethodsList = new Set(list); } /** *set an authentication method * * @param {number} method method defined in AUTHENTICATION * @returns {boolean} */ deleteSocks5AuthMethod(method) { return this.socks5.authMethodsList.delete(method); } /** *add an user for USERPASS auth method * * @param {string} user * @param {string} pass */ setSocks5UserPass(user, pass) { if (typeof user !== 'string' || typeof pass !== 'string') throw (new TypeError('Invalid username or password')); this.socks5.authConf.userpass.set(user, pass); let methodList = this.socks5.authMethodsList; if (!methodList.has(AUTHENTICATION.USERPASS)) { methodList.add(AUTHENTICATION.USERPASS); } if (methodList.has(AUTHENTICATION.NOAUTH)) { methodList.delete(AUTHENTICATION.NOAUTH); } } /** *delete an user for USERPASS auth method * * @param {string} user * @returns {boolean} */ deleteSocks5UserPass(user) { return this.socks5.authConf.userpass.delete(user); } /** *handle socks handshake *@private * @param {net.Socket} socket * @param {Buffer} chunk */ _handshake(socket, chunk) { if (!this.enabledVersion.has(chunk[0])) { socket.end(); socket.emit('socks_error', `handshake: not enabled version: ${chunk[0]}`); } if (chunk[0] == SOCKS_VERSION5) { this._handshake5(socket, chunk); } else if (chunk[0] == SOCKS_VERSION4) { this._handshake4(socket, chunk); } else { socket.end(); socket.emit('socks_error', `handshake: socks version not supported: ${chunk[0]}`); } } /** *handle socks4 handshake *@private * @param {net.Socket} socket * @param {Buffer} chunk */ _handshake4(socket, chunk) {// SOCKS4/4a let cmd = chunk[1], address, port, uid; port = chunk.readUInt16BE(2); // SOCKS4a if ((chunk[4] === 0 && chunk[5] === chunk[6] === 0) && (chunk[7] !== 0)) { var it = 0; uid = ''; for (it = 0; it < 1024; it++) { uid += String.fromCharCode(chunk[8 + it]); if (chunk[8 + it] === 0x00) break; } address = ''; if (chunk[8 + it] === 0x00) { for (it++; it < 2048; it++) { address += String.fromCharCode(chunk[8 + it]); if (chunk[8 + it] === 0x00) break; } } if (chunk[8 + it] === 0x00) { // DNS lookup DNS.lookup(address, (err, ip, family) => { if (err || family !== 4) { socket.end(_005B); socket.emit('socks_error', err || new Error('only ipv4 allowed')); return; } else { socket.socksAddress = ip; socket.socksPort = port; socket.socksUid = uid; if (cmd == REQUEST_CMD.CONNECT) { socket.request = chunk; this.emit('tcp', socket, ip, port, _CMD_REPLY4.bind(socket)); } else { socket.end(_005B); return; } } }); } else { socket.end(_005B); return; } } else { // SOCKS4 address = `${chunk[4]}.${chunk[5]}.${chunk[6]}.${chunk[7]}`; uid = ''; for (it = 0; it < 1024; it++) { uid += chunk[8 + it]; if (chunk[8 + it] == 0x00) break; } socket.socksAddress = address; socket.socksPort = port; socket.socksUid = uid; if (cmd == REQUEST_CMD.CONNECT) { socket.request = chunk; this.emit('tcp', socket, address, port, _CMD_REPLY4.bind(socket)); } else { socket.end(_005B); return; } } } /** *handle socks5 handshake *@private * @param {net.Socket} socket * @param {Buffer} chunk */ _handshake5(socket, chunk) { let method_count = 0; // Number of authentication methods method_count = chunk[1]; if (chunk.byteLength < method_count + 2) { socket.end(); socket.emit('socks_error', 'socks5 handshake error: too short chunk'); return; } let clientAvailableAuthMethods = []; // i starts on 2, since we've read chunk 0 & 1 already for (let i = 2; i < method_count + 2; i++) { if (this.socks5.authMethodsList.has(chunk[i])) { clientAvailableAuthMethods.push(chunk[i]); } } let resp = Buffer.from([ SOCKS_VERSION5,//response version 5 clientAvailableAuthMethods[0]//select the first auth method ]); let authFunc = this.socks5.authFunc.get(resp[1]); if (clientAvailableAuthMethods.length === 0 || !authFunc) {//no available auth method resp[1] = AUTHENTICATION.NONE; socket.end(resp); socket.emit('socks_error', 'unsupported authentication method'); return; } // auth socket.once('data', chunk => { authFunc.call(this, socket, chunk); }); socket.write(resp);//socks5 auth response } /** *handle socks5 user password auth request *@private * @param {net.Socket} socket * @param {Buffer} chunk */ _socks5UserPassAuth(socket, chunk) { let username, password; // Wrong version! if (chunk[0] !== 1) { // MUST be 1 socket.end(_0101); socket.emit('socks_error', `socks5 handleAuthRequest: wrong socks version: ${chunk[0]}`); return; } try { let na = [], pa = [], ni, pi; for (ni = 2; ni < (2 + chunk[1]); ni++) na.push(chunk[ni]); username = Buffer.from(na).toString('utf8'); for (pi = ni + 1; pi < (ni + 1 + chunk[ni]); pi++) pa.push(chunk[pi]); password = Buffer.from(pa).toString('utf8'); } catch (e) { socket.end(_0101); socket.emit('socks_error', `socks5 handleAuthRequest: username/password ${e}`); return; } // check user:pass let users = this.socks5.authConf.userpass; if (users && users.has(username) && users.get(username) === password) { socket.once('data', chunk => { this._socks5HandleRequest(socket, chunk); }); socket.write(_0100);//success } else { setTimeout(() => { socket.end(_0101);//failed socket.emit('socks_error', `socks5 handleConnRequest: auth failed`); }, Math.floor(Math.random() * 90 + 3)); return; } } /** *handle socks5 no auth request *@private * @param {net.Socket} socket * @param {Buffer} chunk */ _socks5NoAuth(socket, chunk) { this._socks5HandleRequest(socket, chunk); } /** *handle socks5 command request *@private * @param {net.Socket} socket the socks5 request socket * @param {Buffer} chunk the chunk is the cmd request head */ _socks5HandleRequest(socket, chunk) { let cmd = chunk[1],//command address, port; // offset = 3; if (!this.enabledCmd.has(cmd)) { _CMD_REPLY5.call(socket, SOCKS_REPLY.COMMAND_NOT_SUPPORTED);//Command not supported return; } try { address = Address.read(chunk, 3); port = Port.read(chunk, 3); } catch (e) { socket.end(); socket.emit('socks_error', e); return; } socket.targetAddress = address; socket.targetPort = port; if (cmd === REQUEST_CMD.CONNECT) { socket.request = chunk; this.emit('tcp', socket, address, port, _CMD_REPLY5.bind(socket)); } else if (cmd === REQUEST_CMD.UDP_ASSOCIATE) { socket.request = chunk; this.emit('udp', socket, address, port, _CMD_REPLY5.bind(socket)); } else { socket.end(_0507); return; } } } /** *base class for relaies * * @class Relay * @extends {events} */ class Relay extends events { socket; relaySocket; closed = false; get localAddress() { return this.relaySocket && this.relaySocket.address().address; } get localPort() { return this.relaySocket && this.relaySocket.address().port; } constructor(socket) { super(); this.socket = socket; } close() { if (this.closed) return; this.closed = true; this.emit('close'); setImmediate(() => { this.socket && destroySocket(this.socket); this.relaySocket && destroySocket(this.relaySocket); this.relaySocket = null; this.socket = null; }); } } /** *an udp relay tool * * @class UDPRelay * @extends {Relay} */ class UDPRelay extends Relay { packetHandler; /** * Creates an instance of UDPRelay. * @param {net.Socket} socket the socks5 request socket * @param {string} address client's outgoing address, the address must be an IP * @param {number} port client's outgoing port * @param {function} CMD_REPLY CMD_REPLY(reply_code) */ constructor(socket, address, port, CMD_REPLY) { super(socket); this.relaySocket = null;//the UDP socket used for relay udp request //the client want to send UDP datagrams from this address to this relay //if not clarified, the first incoming address will be the client's address this.expectClientAddress = address; this.expectClientPort = port; //the client's address and port which finally determined this.finalClientAddress = null; this.finalClientPort = null; let ipFamily; if (net.isIPv4(socket.localAddress)) { ipFamily = 4; } else if (net.isIPv6(socket.localAddress)) { ipFamily = 6; } else { CMD_REPLY(SOCKS_REPLY.ADDR_NOT_SUPPORTED);//Address type not supported return; } const relaySocket = this.relaySocket = dgram.createSocket('udp' + ipFamily);//create a relay socket to targets relaySocket.bind(() => { CMD_REPLY(SOCKS_REPLY.SUCCEEDED, ipFamily === 4 ? '0.0.0.0' : "::", this.localPort);//success }); relaySocket.on('message', async (msg, info) => {//message from remote or client /* only handle datagrams from socket source and specified address */ if (this._isFromClient(info)) {//from client to remote let headLength = UDPRelay.validateSocks5UDPHead(msg); if (!headLength) return; //unpack the socks5 udp request let packet = { address: Address.read(msg, 3), port: Port.read(msg, 3), data: msg.subarray(headLength), }; this.emit('message', true, packet); if (this.packetHandler) await this.packetHandler(true, packet); this.relaySocket.send(packet.data, packet.port, packet.address, err => { if (err) this.emit('proxy_error', relaySocket, 'to remote', err); }); } else {//from other hosts if (!this.finalClientAddress) return;//ignore if client address unknown let packet = { address: info.address, port: info.port, data: msg, }; this.emit('message', false, packet); if (this.packetHandler) await this.packetHandler(false, packet); this.reply(info.address, info.port, packet.data, err => { if (err) this.emit('proxy_error', relaySocket, 'to client', err); }); } }).once('error', e => { if (!CMD_REPLY(SOCKS_REPLY.HOST_UNREACHABLE)) socket.destroy('relay error'); }); //when the tcp socket ends, the relay must stop socket.once('close', () => { this.close(); }); } /** *reply message from remote to client * * @param {string} address message source * @param {number} port * @param {Buffer} msg message * @param {function} callback */ reply(address, port, msg, callback) { let head = replyHead5(address, port); head[0] = 0x00; this.relaySocket.send(Buffer.concat([head, msg]), this.finalClientPort, this.finalClientAddress, callback); } /** *check if the message is from socks client * * @param {dgram.RemoteInfo} info * @returns {boolean} */ _isFromClient(info) { if (!this.finalClientPort) {//update client's out going address and port by the first client message if (this.expectClientPort && info.port !== this.expectClientPort) {//if client port defined but not from expectClientPort return false; } if (info.address !== this.socket.remoteAddress) {//not from client, ignore it return false; } //the first request from client this.finalClientAddress = info.address; this.finalClientPort = info.port; return true; } if (this.finalClientAddress !== info.address || this.finalClientPort !== info.port) return false; return true; } close() { super.close(); this.packetHandler = null; } /** *check socks5 UDP head * * @static * @param {Buffer} buf * @returns {boolean|number} return false if it's not a valid head,otherwise return the head size */ static validateSocks5UDPHead(buf) { if (buf[0] !== 0 || buf[1] !== 0) return false; let minLength = 6;//data length without addr if (buf[3] === 0x01) { minLength += 4; } else if (buf[3] === 0x03) { minLength += buf[4]; } else if (buf[3] === 0x04) { minLength += 16; } else return false; if (buf.byteLength < minLength) return false; return minLength; } } /** *an tcp relay tool * * @class TCPRelay * @extends {Relay} */ class TCPRelay extends Relay { remoteAddress; remotePort; outModifier;//a readable stream for modifying outgoing stream inModifier;//a readable stream for modifying incoming stream /** * Creates an instance of TCPRelay. * @param {net.Socket} socket the socks5 request socket * @param {string} remoteAddress target server's address * @param {number} remotePort target server's port * @param {function} CMD_REPLY CMD_REPLY(reply_code) * @param {string} [localAddress] bind on this address for relay socket * @param {number} [localPort] bind on this port for relay socket */ constructor(socket, remoteAddress, remotePort, CMD_REPLY, localAddress, localPort) { super(socket); this.remoteAddress = remoteAddress; this.remotePort = remotePort; this.socket = socket; const relaySocket = this.relaySocket = net.createConnection({//the tcp socket used for relay tcp request port: remotePort, host: remoteAddress, localAddress: localAddress || undefined, localPort: localPort || undefined }); relaySocket.on('connect', () => { CMD_REPLY(SOCKS_REPLY.SUCCEEDED, this.localAddress, this.localPort); let outChain = [socket, relaySocket]; if (this.outModifier) outChain.splice(1, 0, this.outModifier); pipeline(outChain, (err) => { if (err) { this.emit('socks_error', err); } }); let inChain = [relaySocket, socket]; if (this.inModifier) inChain.splice(1, 0, this.inModifier); pipeline(inChain, (err) => { if (err) { this.emit('socks_error', err); } }); this.emit('connection', socket, relaySocket); }).once('error', err => { let rep = SOCKS_REPLY.SERVER_FAILURE; if (err.message.indexOf('ECONNREFUSED') > -1) { rep = SOCKS_REPLY.CONNECTION_REFUSED; } else if (err.message.indexOf('EHOSTUNREACH') > -1) { rep = SOCKS_REPLY.HOST_UNREACHABLE; } else if (err.message.indexOf('ENETUNREACH') > -1) { rep = SOCKS_REPLY.NETWORK_UNREACHABLE; } CMD_REPLY(rep); this.emit('proxy_error', err, socket, relaySocket); this.close(); }); socket.once('close', () => { this.close(); }); } close() { super.close(); this.inModifier = null; this.outModifier = null; this.packetHandler = null; } } const _0000 = Buffer.from([0, 0, 0, 0]), _00 = Buffer.from([0, 0]); function replyHead5(addr, port) { let resp = [0x05, 0x00, 0x00]; if (!addr || net.isIPv4(addr)) { resp.push(0x01, ...(addr ? (new ipAddress.Address4(addr)).toArray() : _0000)); } else if (net.isIPv6(addr)) { resp.push(0x04, ...((new ipAddress.Address6(addr)).toUnsignedByteArray())); } else { addr = Buffer.from(addr); if (addr.byteLength > 255) throw (new Error('too long domain name')); resp.push(0x03, addr.byteLength, ...addr); } if (!port) resp.push(0, 0);//default:0 else { resp.push(port >>> 8, port & 0xFF); } return Buffer.from(resp); } function _CMD_REPLY5(REP, addr, port) {//'this' is the socket if (this.CMD_REPLIED || !this.writable) return false;//prevent it from replying twice // creating response if (REP) {//something wrong this.end(Buffer.from([0x05, REP, 0x00])); } else { this.write(replyHead5(addr, port)); } this.CMD_REPLIED = true; return true; } function _CMD_REPLY4() {//'this' is the socket if (this.CMD_REPLIED) return; // creating response let resp = Buffer.allocUnsafe(8); // write response header resp[0] = 0x00; resp[1] = 0x5a; // port resp.writeUInt16BE(this.socksPort, 2); // ip let ips = this.socksAddress.split('.'); resp.writeUInt8(parseInt(ips[0]), 4); resp.writeUInt8(parseInt(ips[1]), 5); resp.writeUInt8(parseInt(ips[2]), 6); resp.writeUInt8(parseInt(ips[3]), 7); this.write(resp); this.CMD_REPLIED = true; } function destroySocket(socket) { if (socket.destroyed === false) { socket.destroy(); } socket.removeAllListeners(); } function createSocksServer(options) { return new socksServer(options); } module.exports = { createSocksServer, socksServer, replyHead5, UDPRelay, TCPRelay, Address, Port, AUTHENTICATION, REQUEST_CMD, SOCKS_REPLY, };