From 9e0dafcaa0a723c8a26072b1ca210ddfaaed8609 Mon Sep 17 00:00:00 2001 From: VincentChanX <710852740@qq.com> Date: Wed, 5 Apr 2017 19:41:04 +0800 Subject: [PATCH] init --- config.json | 8 ++ local.js | 9 ++ package.json | 28 ++++ server.js | 9 ++ tcprelay.js | 356 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 410 insertions(+) create mode 100644 config.json create mode 100644 local.js create mode 100644 package.json create mode 100644 server.js create mode 100644 tcprelay.js diff --git a/config.json b/config.json new file mode 100644 index 0000000..6f45e03 --- /dev/null +++ b/config.json @@ -0,0 +1,8 @@ +{ + "localAddress":"127.0.0.1", + "localPort": 1080, + "serverAddress": "52.221.230.93", + "serverPort": 21001, + "password": "ourvpn01010101", + "method": "aes-256-cfb" +} \ No newline at end of file diff --git a/local.js b/local.js new file mode 100644 index 0000000..973ce4c --- /dev/null +++ b/local.js @@ -0,0 +1,9 @@ +const {TCPRelay} = require('./tcprelay'); +new TCPRelay({ + localAddress: '127.0.0.1', + localPort: 1080, + serverAddress: '127.0.0.1', + serverPort: 21001, + password: 'ourvpn01010101', + method: "aes-256-cfb" +}, true, 'error').bootstrap(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..d0058f0 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "shadowsocks-over-websocket", + "version": "0.1.0", + "description": "A fast tunnel proxy that helps you bypass firewalls", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "server": "node server.js", + "local": "node local.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/VincentChanX/shadowsocks-over-websocket.git" + }, + "keywords": [ + "proxy" + ], + "author": "vincentchan (https://github.com/VincentChanX)", + "license": "GPL-3.0", + "bugs": { + "url": "https://github.com/VincentChanX/shadowsocks-over-websocket/issues" + }, + "homepage": "https://github.com/VincentChanX/shadowsocks-over-websocket#readme", + "dependencies": { + "log4js": "^1.1.1", + "ws": "^2.2.3" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..c33ebfc --- /dev/null +++ b/server.js @@ -0,0 +1,9 @@ +const {TCPRelay} = require('./tcprelay'); +new TCPRelay({ + localAddress: '127.0.0.1', + localPort: 1080, + serverAddress: '127.0.0.1', + serverPort: 21001, + password: 'ourvpn01010101', + method: "aes-256-cfb" +}, false, 'error').bootstrap(); \ No newline at end of file diff --git a/tcprelay.js b/tcprelay.js new file mode 100644 index 0000000..7172e5c --- /dev/null +++ b/tcprelay.js @@ -0,0 +1,356 @@ +const net = require('net'); +const log4js = require('log4js'); +const WebSocket = require('ws'); +const Encryptor = require('shadowsocks/lib/shadowsocks/encrypt').Encryptor; + +const MAX_CONNECTIONS = 50000; + +const TCP_RELAY_TYPE_LOCAL = 1; +const TCP_RELAY_TYPE_SERVER = 2; + +const ADDRESS_TYPE_IPV4 = 0x01; +const ADDRESS_TYPE_DOMAIN_NAME = 0x03; +const ADDRESS_TYPE_IPV6 = 0x04; +const ADDRESS_TYPE = { + 1: 'IPV4', + 3: 'DOMAIN_NAME', + 4: 'IPV6' +}; + +const VERSION = 0x05; + +const METHOD_NO_AUTHENTICATION_REQUIRED = 0x00; +const METHOD_GSSAPI = 0x01; +const METHOD_USERNAME_PASSWORD = 0x02; +const METHOD_NO_ACCEPTABLE_METHODS = 0xff; + +const CMD_CONNECT = 0x01; +const CMD_BIND = 0x02; +const CMD_UDP_ASSOCIATE = 0x03; +const CMD = { + 1: 'CONNECT', + 2: 'BIND', + 3: 'UDP_ASSOCIATE' +}; + +const REPLIE_SUCCEEDED = 0x00; +const REPLIE_GENERAL_SOCKS_SERVER_FAILURE = 0x01; +const REPLIE_CONNECTION_NOT_ALLOWED_BY_RULESET = 0x02; +const REPLIE_NETWORK_UNREACHABLE = 0x03; +const REPLIE_HOST_UNREACHABLE = 0x04; +const REPLIE_CONNECTION_REFUSED = 0x05; +const REPLIE_TTL_EXPIRED = 0x06; +const REPLIE_COMMAND_NOT_SUPPORTED = 0x07; +const REPLIE_ADDRESS_TYPE_NOT_SUPPORTED = 0x08; + +const STAGE_INIT = 0; +const STAGE_ADDR = 1; +const STAGE_UDP_ASSOC = 2; +const STAGE_DNS = 3; +const STAGE_CONNECTING = 4; +const STAGE_STREAM = 5; +const STAGE_DESTROYED = -1; + +const STAGE = { + [-1]: 'STAGE_DESTROYED', + 0: 'STAGE_INIT', + 1: 'STAGE_ADDR', + 2: 'STAGE_UDP_ASSOC', + 3: 'STAGE_DNS', + 4: 'STAGE_CONNECTING', + 5: 'STAGE_STREAM' +}; + +var globalConnectionId = 1; + +function parseAddressHeader(data, offset) { + var addressType = data.readUInt8(offset); + var headerLen, dstAddr, dstPort, dstAddrLen; + if (addressType == ADDRESS_TYPE_DOMAIN_NAME) { + dstAddrLen = data.readUInt8(offset + 1); + dstAddr = data.slice(offset + 2, offset + 2 + dstAddrLen).toString(); + dstPort = data.readUInt16BE(offset + 2 + dstAddrLen); + headerLen = 4 + dstAddrLen; + } + //ipv4 + else if (addressType == ADDRESS_TYPE_IPV4) { + dstAddr = data.slice(offset + 1, offset + 5).join('.').toString(); + dstPort = data.readUInt16BE(offset + 5); + headerLen = 7; + } else { + return false; + } + return { + addressType: addressType, + headerLen: headerLen, + dstAddr: dstAddr, + dstPort: dstPort + }; +} + +// client <=> local <=> server <=> target +function TCPRelay(config, isLocal, logLevel) { + this.isLocal = isLocal; + this.server = null; + this.config = require('./config.json'); + if (config) { + this.config = Object.assign(this.config, config); + } + this.logger = log4js.getLogger(isLocal ? 'sslocal' : 'ssserver'); + this.logger.setLevel(logLevel ? logLevel : 'error'); +} + + +TCPRelay.prototype.getServerName = function() { + return this.isLocal ? 'sslocal' : 'ssserver'; +}; + + +TCPRelay.prototype.bootstrap = function() { + this.init(); +}; + +TCPRelay.prototype.init = function() { + var self = this; + var config = self.config; + var port = self.isLocal ? config.localPort : config.serverPort; + var address = self.isLocal ? config.localAddress : config.serverAddress; + var server; + + if (self.isLocal) { + server = self.server = net.createServer({ + allowHalfOpen: true, + }); + server.maxConnections = MAX_CONNECTIONS; + server.on('connection', function(connection) { + return self.handleConnectionByLocal(connection); + }); + server.listen(port, address); + } else { + server = self.server = new WebSocket.Server({ + host: address, + port: port, + verifyClient: false + }); + server.on('connection', function(connection) { + return self.handleConnectionByServer(connection); + }); + } + server.on('error', function(error) { + self.logger.error('an error of', self.getServerName(), 'occured', error); + }); + server.on('listening', function() { + self.logger.info(self.getServerName(), 'is listening on', address + ':' + port); + }); +}; + +//server +TCPRelay.prototype.handleConnectionByServer = function(connection) { + var self = this; + var config = self.config; + var method = config.method; + var password = config.password; + var serverAddress = config.serverAddress; + var serverPort = config.serverPort; + + var logger = self.logger; + var encryptor = new Encryptor(password, method); + + var stage = STAGE_INIT; + var connectionId = (globalConnectionId++) % MAX_CONNECTIONS; + var targetConnection, addressHeader; + + var canWriteToLocalConnection = true; + + logger.info(`accept connection from local[${connectionId}]`); + connection.on('message', function(data) { + data = encryptor.decrypt(data); + logger.info(`read data[${data.length}] from local connection[${connectionId}] at stage[${STAGE[stage]}]`); + + switch (stage) { + + case STAGE_INIT: + if (data.length < 7) { + stage = STAGE_DESTROYED; + return connection.close(); + } + addressHeader = parseAddressHeader(data, 0); + if (!addressHeader) { + stage = STAGE_DESTROYED; + return connection.close(); + } + + logger.info(`connecting to ${addressHeader.dstAddr}:${addressHeader.dstPort}`); + + connection.pause(); + + targetConnection = net.createConnection({ + port: addressHeader.dstPort, + host: addressHeader.dstAddr, + allowHalfOpen: true + }, function() { + logger.info(`connecting to target[${connectionId}]`); + connection.resume(); + stage = STAGE_STREAM; + }); + + targetConnection.on('data', function(data) { + logger.info(`read data[${data.length}] from target connection[${connectionId}]`); + logger.info(`write data[${data.length}] to local connection[${connectionId}]`); + // targetConnection.pause(); + canWriteToLocalConnection && connection.send(encryptor.encrypt(data), function() { + // targetConnection.resume(); + }); + }); + targetConnection.setKeepAlive(true, 4000); + targetConnection.on('end', function() { + connection.close(); + }); + targetConnection.on('error', function(error) { + logger.error(`an error of target connection[${connectionId}] occured`, error); + targetConnection.destroy(); + connection.close(); + }); + + if (data.length > addressHeader.headerLen) { + connection.pause(); + targetConnection.write(data.slice(addressHeader.headerLen), function() { + connection.resume(); + }); + } + stage = STAGE_CONNECTING; + break; + + case STAGE_STREAM: + connection.pause(); + canWriteToLocalConnection && targetConnection.write(data, function() { + logger.info(`write data[${data.length}] to target connection[${connectionId}]`); + connection.resume(); + }); + break; + } + }); + connection.on('close', function(hadError) { + logger.info(`close event[${hadError}] of connection[${connectionId}] has been triggered`); + canWriteToLocalConnection = false; + }); + connection.on('error', function(error) { + logger.error(`an error of connection[${connectionId}] occured`, error); + connection.terminate(); + canWriteToLocalConnection = false; + targetConnection && targetConnection.end(); + }); +} + + + +//local +TCPRelay.prototype.handleConnectionByLocal = function(connection) { + var self = this; + var config = self.config; + var method = config.method; + var password = config.password; + var serverAddress = config.serverAddress; + var serverPort = config.serverPort; + + var logger = self.logger; + var encryptor = new Encryptor(password, method); + + var stage = STAGE_INIT; + var connectionId = (globalConnectionId++) % MAX_CONNECTIONS; + var serverConnection, cmd, addressHeader; + + var canWriteToLocalConnection = true; + + logger.info(`accept connection from client[${connectionId}]`); + connection.setKeepAlive(true, 10000); + connection.on('data', function(data) { + logger.info(`read data[${data.length}] from client connection[${connectionId}] at stage[${STAGE[stage]}]`); + switch (stage) { + + case STAGE_INIT: + if (data.length < 3 || data.readUInt8(0) != 5) { + stage = STAGE_DESTROYED; + return connection.end(); + } + connection.write("\x05\x00"); + stage = STAGE_ADDR; + break; + + case STAGE_ADDR: + if (data.length < 10 || data.readUInt8(0) != 5) { + stage = STAGE_DESTROYED; + return connection.end(); + } + cmd = data.readUInt8(1); + addressHeader = parseAddressHeader(data, 3); + if (!addressHeader) { + stage = STAGE_DESTROYED; + return connection.end(); + } + + //only supports connect cmd + if (cmd != CMD_CONNECT) { + logger.error('only supports connect cmd'); + return connection.end("\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00"); + } + + logger.info(`connecting to ${addressHeader.dstAddr}:${addressHeader.dstPort}`); + connection.write("\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00"); + + connection.pause(); + + serverConnection = new WebSocket('ws://' + serverAddress + ':' + serverPort, { + perMessageDeflate: true + }); + serverConnection.on('open', function() { + logger.info(`connecting to websocket server[${connectionId}]`); + serverConnection.send(encryptor.encrypt(data.slice(3)), function() { + connection.resume(); + stage = STAGE_STREAM; + }); + }); + serverConnection.on('message', function(data) { + logger.info(`read data[${data.length}] from websocket server connection[${connectionId}]`); + logger.info(`write data[${data.length}] to client connection[${connectionId}]`); + // serverConnection.pause(); + canWriteToLocalConnection && connection.write(encryptor.decrypt(data), function() { + // serverConnection.resume(); + }); + }); + serverConnection.on('error', function(error) { + logger.error(`an error of server connection[${connectionId}] occured`, error); + connection.end(); + }); + serverConnection.on('close', function() { + connection.end(); + }); + + stage = STAGE_CONNECTING; + break; + + case STAGE_STREAM: + connection.pause(); + canWriteToLocalConnection && serverConnection.send(encryptor.encrypt(data), function() { + logger.info(`write data[${data.length}] to websocket server connection[${connectionId}]`); + connection.resume(); + }); + break; + } + }); + connection.on('end', function() { + logger.info(`end event of client connection[$connectionId] has been triggered`); + }); + connection.on('close', function(hadError) { + logger.info(`close event[${hadError}] of client connection[${connectionId}] has been triggered`); + canWriteToLocalConnection = false; + }); + connection.on('error', function(error) { + logger.error(`an error of client connection[${connectionId}] occured`, error); + connection.destroy(); + canWriteToLocalConnection = false; + serverConnection && serverConnection.close(); + }); +} + +module.exports.TCPRelay = TCPRelay; \ No newline at end of file