socks5
Version:
A simple SOCKS 5/4/4a implementation and demo proxy
392 lines (350 loc) • 11.9 kB
JavaScript
'use strict'
var net = require('net'),
util = require('util'),
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
************************
* 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
*/
AUTHENTICATION = {
NOAUTH: 0x00,
GSSAPI: 0x01,
USERPASS: 0x02,
NONE: 0xFF
},
/*
* o CMD
* o CONNECT X'01'
* o BIND X'02'
* o UDP ASSOCIATE X'03'
*/
REQUEST_CMD = {
CONNECT: 0x01,
BIND: 0x02,
UDP_ASSOCIATE: 0x03
},
/*
* o ATYP address type of following address
* o IP V4 address: X'01'
* o DOMAINNAME: X'03'
* o IP V6 address: X'04'
*/
ATYP = {
IP_V4: 0x01,
DNS: 0x03,
IP_V6: 0x04
},
Address = {
read: function (buffer, offset) {
if (buffer[offset] == ATYP.IP_V4) {
return util.format('%s.%s.%s.%s', 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) {
return buffer.slice(buffer[offset+1], buffer[offset+1+16]);
}
},
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) {
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, userpass) {
// record userpass
USERPASS = userpass;
console.log('userpass:'+JSON.stringify(userpass));
var socksServer = net.createServer();
socksServer.on('listening', function() {
var address = socksServer.address();
console.log('LISTENING %s:%d', address.address, address.port);
});
socksServer.on('connection', function(socket) {
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);
// remove from clients on disconnect
this.on('end', function() {
var idx = clients.indexOf(this);
if (idx != -1) {
clients.splice(idx, 1);
}
});*/
this.on('error', function(e) {
errorLog('%j', e);
});
// 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.once('data', this.handshake);
}
function handshake(chunk) {
// 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_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 2, since we've read chunk 0 & 1 already
for (var i=2; i < method_count + 2; i++) {
this.auth_methods.push(chunk[i]);
}
log('Supported auth methods: %j', this.auth_methods);
var resp = new Buffer(2);
resp[0] = 0x05;
// 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 {
// 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 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_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('socks5 handleConnRequest: Mangled request. Reserved field is not null: %d', chunk[offset]);
return;
} */
try {
address = Address.read(chunk, 3);
port = Port.read(chunk, 3);
} catch (e) {
errorLog('socks5 handleConnRequest: Address.read '+e);
return;
}
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, proxyReady5.bind(this));
} else {
this.end(new Buffer([0x05, 0x01]));
return;
}
}
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_VERSION5;
resp[1] = 0x00;
resp[2] = 0x00;
this.write(resp);
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 = {
createServer: createSocksServer
};