diff --git a/AUTHORS b/AUTHORS index 89d6bec..2576fc0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,2 +1,3 @@ Gert Van Gool John Cant +Tom Zhou diff --git a/README.md b/README.md new file mode 100644 index 0000000..412ca72 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +SOCKS v4/v4a/v5 server implementation with user/pass authentication node.js +============================================================================= + +A simple SOCKS v5/v4/v4a server implementation and demo proxy. + +You can run it easily as:: + + node proxy.js + +This will create a proxy at ``127.0.0.1`` on port ``8888``. + +You can use this as a good starting point for writing a proxy or a tunnel! + +### Install +- npm install socks5 + + +### License + +(The MIT License) diff --git a/README.rst b/README.rst deleted file mode 100644 index b3af383..0000000 --- a/README.rst +++ /dev/null @@ -1,12 +0,0 @@ -SOCKS implementation in node.js -=============================== - -A simple SOCKS implementation and demo proxy in `node.js `_. - -You can run it easily as:: - - ./socks - -This will create a proxy at ``127.0.0.1`` on port ``8888``. - -You can use this as a good starting point for writing a proxy or a tunnel! diff --git a/package.json b/package.json index a1457e2..af08fe3 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,24 @@ { - "name": "node-socks" - , "version": "0.1.0" - , "description": "A simple SOCKS implementation and demo proxy" - , "author": "Gert Van Gool " + "name": "socks5" + , "version": "1.0.1" + , "description": "A simple SOCKS 5/4/4a implementation and demo proxy" + , "author": [ + "Gert Van Gool ", + "Tom Zhou " + ] + , "repository": { + "type": "git", + "url": "git://github.com/sequoiar/socks5.git" + } + , "keywords": [ + "socks", + "socks5", + "socks4", + "socks4a", + "proxy", + "http" + ] + , "main": "socks.js" , "dependencies": { } } diff --git a/proxy.js b/proxy.js new file mode 100644 index 0000000..882b02f --- /dev/null +++ b/proxy.js @@ -0,0 +1,100 @@ + +var net = require('net'), + socks = require('./socks.js'), + info = console.log.bind(console); + +// Create server +// The server accepts SOCKS connections. This particular server acts as a proxy. +var HOST='127.0.0.1', + PORT='8888', + server = socks.createServer(function(socket, port, address, proxy_ready) { + + // Implement your own proxy here! Do encryption, tunnelling, whatever! Go flippin' mental! + // I plan to tunnel everything including SSH over an HTTP tunnel. For now, though, here is the plain proxy: + + var proxy = net.createConnection({port:port, host:address,localAddress:process.argv[2]||undefined}, proxy_ready); + var localAddress,localPort; + proxy.on('connect', function(){ + info('%s:%d <== %s:%d ==> %s:%d',socket.remoteAddress,socket.remotePort, + proxy.localAddress,proxy.localPort,proxy.remoteAddress,proxy.remotePort); + localAddress=proxy.localAddress; + localPort=proxy.localPort; + }.bind(this)); + proxy.on('data', function(d) { + try { + //console.log('receiving ' + d.length + ' bytes from proxy'); + if (!socket.write(d)) { + proxy.pause(); + + socket.on('drain', function(){ + proxy.resume(); + }); + setTimeout(function(){ + proxy.resume(); + }, 100); + } + } catch(err) { + } + }); + socket.on('data', function(d) { + // If the application tries to send data before the proxy is ready, then that is it's own problem. + try { + //console.log('sending ' + d.length + ' bytes to proxy'); + if (!proxy.write(d)) { + socket.pause(); + + proxy.on('drain', function(){ + socket.resume(); + }); + setTimeout(function(){ + socket.resume(); + }, 100); + } + } catch(err) { + } + }); + + proxy.on('error', function(err){ + //console.log('Ignore proxy error'); + }); + proxy.on('close', function(had_error) { + try { + if(localAddress && localPort) + console.log('The proxy %s:%d closed', localAddress, localPort); + else + console.error('Connect to %s:%d failed', address, port); + socket.end(); + } catch (err) { + } + }.bind(this)); + + socket.on('error', function(err){ + //console.log('Ignore socket error'); + }); + socket.on('close', function(had_error) { + try { + if (this.proxy !== undefined) { + proxy.removeAllListeners('data'); + proxy.end(); + } + //console.error('The socket %s:%d closed',socket.remoteAddress,socket.remotePort); + } catch (err) { + } + }.bind(this)); + + }, process.argv[3]&&process.argv[4]&&{username:process.argv[3],password:process.argv[4]}); + +server.on('error', function (e) { + console.error('SERVER ERROR: %j', e); + if (e.code == 'EADDRINUSE') { + console.log('Address in use, retrying in 10 seconds...'); + setTimeout(function () { + console.log('Reconnecting to %s:%s', HOST, PORT); + server.close(); + server.listen(PORT, HOST); + }, 10000); + } +}); +server.listen(PORT, HOST); + +// vim: set filetype=javascript syntax=javascript : diff --git a/socks b/socks deleted file mode 100755 index 9c46f3a..0000000 --- a/socks +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env node -var net = require('net'), - socks = require('./socks.js'); - -// Create server -// The server accepts SOCKS connections. This particular server acts as a proxy. -var HOST='127.0.0.1', - PORT='8888', - server = socks.createServer(function(socket, port, address, proxy_ready) { - - // Implement your own proxy here! Do encryption, tunnelling, whatever! Go flippin' mental! - // I plan to tunnel everything including SSH over an HTTP tunnel. For now, though, here is the plain proxy: - - console.log('Got through the first part of the SOCKS protocol.') - var proxy = net.createConnection(port, address, proxy_ready); - - proxy.on('data', function(d) { - try { - console.log('receiving ' + d.length + ' bytes from proxy'); - socket.write(d); - } catch(err) { - } - }); - socket.on('data', function(d) { - // If the application tries to send data before the proxy is ready, then that is it's own problem. - try { - console.log('sending ' + d.length + ' bytes to proxy'); - proxy.write(d); - } catch(err) { - } - }); - - proxy.on('close', function(had_error) { - socket.end(); - console.error('The proxy closed'); - }.bind(this)); - socket.on('close', function(had_error) { - if (this.proxy !== undefined) { - proxy.removeAllListeners('data'); - proxy.end(); - } - console.error('The application closed'); - }.bind(this)); - - }); - -server.on('error', function (e) { - console.error('SERVER ERROR: %j', e); - if (e.code == 'EADDRINUSE') { - console.log('Address in use, retrying in 10 seconds...'); - setTimeout(function () { - console.log('Reconnecting to %s:%s', HOST, PORT); - server.close(); - server.listen(PORT, HOST); - }, 10000); - } -}); -server.listen(PORT, HOST); - -// vim: set filetype=javascript syntax=javascript : diff --git a/socks.js b/socks.js index 3daeb5e..ee7bb1f 100644 --- a/socks.js +++ b/socks.js @@ -1,12 +1,17 @@ +'use strict' + var net = require('net'), util = require('util'), - log = function(args) { - //console.log(args); - }, - info = console.info, - errorLog = console.error, - clients = [], - SOCKS_VERSION = 5, + DNS = require('dns'), + log = function(){},///console.log, + //log = console.log.bind(console), + //info = console.info.bind(console), + info = function(){},///console.log, + errorLog = console.error.bind(console), + ///clients = [], + SOCKS_VERSION5 = 5, + SOCKS_VERSION4 = 4, + USERPASS, /* * Authentication methods ************************ @@ -64,23 +69,39 @@ var net = require('net'), return 16; } } + }, + Port = { + read: function (buffer, offset) { + 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); + } + }, }; -function createSocksServer(cb) { +function createSocksServer(cb, userpass) { + // record userpass + USERPASS = userpass; + console.log('userpass:'+JSON.stringify(userpass)); + var socksServer = net.createServer(); socksServer.on('listening', function() { var address = socksServer.address(); - info('LISTENING %s:%s', address.address, address.port); + console.log('LISTENING %s:%d', address.address, address.port); }); socksServer.on('connection', function(socket) { - info('CONNECTED %s:%s', socket.remoteAddress, socket.remotePort); + info('CONNECTED %s:%d', socket.remoteAddress, socket.remotePort); initSocksConnection.bind(socket)(cb); }); return socksServer; } -// + // socket is available as this function initSocksConnection(on_accept) { + /* // keep log of connected clients clients.push(this); @@ -90,7 +111,8 @@ function initSocksConnection(on_accept) { if (idx != -1) { clients.splice(idx, 1); } - }); + });*/ + this.on('error', function(e) { errorLog('%j', e); }); @@ -98,24 +120,40 @@ function initSocksConnection(on_accept) { // do a handshake this.handshake = handshake.bind(this); this.on_accept = on_accept; // No bind. We want 'this' to be the server, like it would be for net.createServer - this.on('data', this.handshake); + this.once('data', this.handshake); } function handshake(chunk) { - this.removeListener('data', this.handshake); + // SOCKS Version 4/5 is the only support version + if (chunk[0] == SOCKS_VERSION5) { + this.socksVersion = SOCKS_VERSION5; + this.handshake5 = handshake5.bind(this); + this.handshake5(chunk); + } else if (chunk[0] == SOCKS_VERSION4) { + this.socksVersion = SOCKS_VERSION4; + this.handshake4 = handshake4.bind(this); + this.handshake4(chunk); + } else { + errorLog('handshake: wrong socks version: %d', chunk[0]); + this.end(); + } +} +// SOCKS5 +function handshake5(chunk) { var method_count = 0; // SOCKS Version 5 is the only support version - if (chunk[0] != SOCKS_VERSION) { - errorLog('handshake: wrong socks version: %d', chunk[0]); + if (chunk[0] != SOCKS_VERSION5) { + errorLog('socks5 handshake: wrong socks version: %d', chunk[0]); this.end(); + return; } // Number of authentication methods method_count = chunk[1]; this.auth_methods = []; - // i starts on 1, since we've read chunk 0 & 1 already + // i starts on 2, since we've read chunk 0 & 1 already for (var i=2; i < method_count + 2; i++) { this.auth_methods.push(chunk[i]); } @@ -123,63 +161,229 @@ function handshake(chunk) { var resp = new Buffer(2); resp[0] = 0x05; - if (this.auth_methods.indexOf(AUTHENTICATION.NOAUTH) > -1) { - log('Handing off to handleRequest'); - this.handleRequest = handleRequest.bind(this); - this.on('data', this.handleRequest); - resp[1] = AUTHENTICATION.NOAUTH; - this.write(resp); + + // user/pass auth + if (USERPASS) { + if (this.auth_methods.indexOf(AUTHENTICATION.USERPASS) > -1) { + log('Handing off to handleAuthRequest'); + this.handleAuthRequest = handleAuthRequest.bind(this); + this.once('data', this.handleAuthRequest); + resp[1] = AUTHENTICATION.USERPASS; + this.write(resp); + } else { + errorLog('Unsuported authentication method -- disconnecting'); + resp[1] = 0xFF; + this.end(resp); + } + } else + // NO Auth + if (this.auth_methods.indexOf(AUTHENTICATION.NOAUTH) > -1) { + log('Handing off to handleConnRequest'); + this.handleConnRequest = handleConnRequest.bind(this); + this.once('data', this.handleConnRequest); + resp[1] = AUTHENTICATION.NOAUTH; + this.write(resp); + } else { + errorLog('Unsuported authentication method -- disconnecting'); + resp[1] = 0xFF; + this.end(resp); + } +} + +// SOCKS4/4a +function handshake4(chunk) { + var cmd = chunk[1], + address, + port, + uid; + + // Wrong version! + if (chunk[0] !== SOCKS_VERSION4) { + this.end(new Buffer([0x00, 0x5b])); + errorLog('socks4 handleConnRequest: wrong socks version: %d', chunk[0]); + return; + } + 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 += chunk[8 + it]; + if (chunk[8 + it] == 0x00) + break; + } + address = ''; + if (chunk[8 + it] == 0x00) { + for (it++; it < 2048; it++) { + address += chunk[8 + it]; + if (chunk[8 + it] == 0x00) + break; + } + } + if (chunk[8 + it] == 0x00) { + // DNS lookup + DNS.lookup(address, function (err, ip, family) { + if (err) { + errorLog(err + ',socks4a dns lookup failed'); + + this.end(new Buffer([0x00, 0x5b])); + return; + } else { + this.socksAddress = ip; + this.socksPort = port; + this.socksUid = uid; + + log('socks4a Request: type: %d -- to: %s:%d:%s', cmd, address, port, uid); + + if (cmd == REQUEST_CMD.CONNECT) { + this.request = chunk; + this.on_accept(this, port, ip, proxyReady4.bind(this)); + } else { + this.end(new Buffer([0x00, 0x5b])); + return; + } + } + }); + } else { + this.end(new Buffer([0x00, 0x5b])); + return; + } } else { - errorLog('Unsuported authentication method -- disconnecting'); - resp[1] = 0xFF; - this.end(resp); + // SOCKS4 + address = util.format('%s.%s.%s.%s', 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; + } + + this.socksAddress = address; + this.socksPort = port; + this.socksUid = uid; + + log('socks4 Request: type: %d -- to: %s:%d:%s', cmd, address, port, uid); + + if (cmd == REQUEST_CMD.CONNECT) { + this.request = chunk; + this.on_accept(this, port, address, proxyReady4.bind(this)); + } else { + this.end(new Buffer([0x00, 0x5b])); + return; + } } } -function handleRequest(chunk) { - this.removeListener('data', this.handleRequest); +function handleAuthRequest(chunk) { + var username, + password; + // Wrong version! + if (chunk[0] !== 1) { // MUST be 1 + this.end(new Buffer([0x01, 0x01])); + errorLog('socks5 handleAuthRequest: wrong socks version: %d', chunk[0]); + return; + } + + try { + var na = [],pa=[],ni,pi; + for (ni= 2;ni<(2+chunk[1]); ni++) na.push(chunk[ni]);username = new Buffer(na).toString('utf8'); + for (pi=ni+1;pi<(ni+1+chunk[ni]);pi++) pa.push(chunk[pi]);password = new Buffer(pa).toString('utf8'); + } catch (e) { + this.end(new Buffer([0x01, 0x01])); + errorLog('socks5 handleAuthRequest: username/password '+e); + return; + } + + log('socks5 Auth: username:%s,password:%s', username, password); + + // check user:pass + if (USERPASS && USERPASS.username===username && USERPASS.password===password) { + log('Handing off to handleConnRequest'); + this.handleConnRequest = handleConnRequest.bind(this); + this.once('data', this.handleConnRequest); + this.write(new Buffer([0x01, 0x00])); + } else { + this.end(new Buffer([0x01, 0x01])); + errorLog('socks5 handleConnRequest: wrong socks version: %d', chunk[0]); + return; + } +} + +function handleConnRequest(chunk) { var cmd=chunk[1], address, port, offset=3; // Wrong version! - if (chunk[0] !== SOCKS_VERSION) { - this.end('%d%d', 0x05, 0x01); - errorLog('handleRequest: wrong socks version: %d', chunk[0]); + if (chunk[0] !== SOCKS_VERSION5) { + this.end(new Buffer([0x05, 0x01])); + errorLog('socks5 handleConnRequest: wrong socks version: %d', chunk[0]); return; } /* else if (chunk[2] == 0x00) { this.end(util.format('%d%d', 0x05, 0x01)); - errorLog('handleRequest: Mangled request. Reserved field is not null: %d', chunk[offset]); + errorLog('socks5 handleConnRequest: Mangled request. Reserved field is not null: %d', chunk[offset]); return; } */ - address = Address.read(chunk, 3); - offset = 3 + Address.sizeOf(chunk, 3) + 2; - port = chunk.readUInt16BE(offset); + try { + address = Address.read(chunk, 3); + port = Port.read(chunk, 3); + } catch (e) { + errorLog('socks5 handleConnRequest: Address.read '+e); + return; + } - log('Request: type: %d -- to: %s:%s', chunk[1], address, port); + log('socks5 Request: type: %d -- to: %s:%d', chunk[1], address, port); if (cmd == REQUEST_CMD.CONNECT) { this.request = chunk; - this.on_accept(this, port, address, proxyReady.bind(this)); + this.on_accept(this, port, address, proxyReady5.bind(this)); } else { - this.end('%d%d', 0x05, 0x01); + this.end(new Buffer([0x05, 0x01])); return; } } -function proxyReady() { +function proxyReady5() { log('Indicating to the client that the proxy is ready'); // creating response var resp = new Buffer(this.request.length); this.request.copy(resp); // rewrite response header - resp[0] = SOCKS_VERSION; + resp[0] = SOCKS_VERSION5; resp[1] = 0x00; resp[2] = 0x00; + this.write(resp); - log('Connected to: %s:%d', resp.toString('utf8', 4, resp.length - 2), resp.readUInt16BE(resp.length - 2)); - + + log('socks5 Connected to: %s:%d', Address.read(resp, 3), resp.readUInt16BE(resp.length - 2)); +} +function proxyReady4() { + log('Indicating to the client that the proxy is ready'); + // creating response + var resp = new Buffer(8); + + // write response header + resp[0] = 0x00; + resp[1] = 0x5a; + + // port + resp.writeUInt16BE(this.socksPort, 2); + + // ip + var 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); + + log('socks4 Connected to: %s:%d:%s', this.socksAddress, this.socksPort, this.socksUid); } module.exports = {