2017-04-05 19:41:04 +08:00
|
|
|
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();
|
|
|
|
|
};
|
|
|
|
|
|
2017-04-06 10:44:45 +08:00
|
|
|
TCPRelay.prototype.stop = function() {
|
|
|
|
|
if (this.server) {
|
|
|
|
|
this.server.close();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2017-04-05 19:41:04 +08:00
|
|
|
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,
|
2017-04-06 10:21:58 +08:00
|
|
|
perMessageDeflate: false,
|
|
|
|
|
backlog: MAX_CONNECTIONS
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
|
|
|
|
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;
|
|
|
|
|
|
2017-04-06 10:21:58 +08:00
|
|
|
var dataCache = [];
|
2017-04-05 19:41:04 +08:00
|
|
|
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: accept connection from local`);
|
2017-04-05 19:41:04 +08:00
|
|
|
connection.on('message', function(data) {
|
|
|
|
|
data = encryptor.decrypt(data);
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: read data[length = ${data.length}] from local connection at stage[${STAGE[stage]}]`);
|
2017-04-05 19:41:04 +08:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: connecting to ${addressHeader.dstAddr}:${addressHeader.dstPort}`);
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_CONNECTING;
|
2017-04-05 19:41:04 +08:00
|
|
|
|
|
|
|
|
targetConnection = net.createConnection({
|
|
|
|
|
port: addressHeader.dstPort,
|
|
|
|
|
host: addressHeader.dstAddr,
|
|
|
|
|
allowHalfOpen: true
|
|
|
|
|
}, function() {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: connecting to target`);
|
|
|
|
|
|
|
|
|
|
dataCache = Buffer.concat(dataCache);
|
|
|
|
|
targetConnection.write(dataCache, function() {
|
|
|
|
|
logger.info(`[${connectionId}]: write data[length = ${dataCache.length}] to target connection`);
|
|
|
|
|
dataCache = null;
|
|
|
|
|
});
|
2017-04-05 19:41:04 +08:00
|
|
|
stage = STAGE_STREAM;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
targetConnection.on('data', function(data) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: read data[length = ${data.length}] from target connection`);
|
|
|
|
|
if (connection.readyState == WebSocket.OPEN) {
|
|
|
|
|
connection.send(encryptor.encrypt(data), {
|
|
|
|
|
binary: true
|
|
|
|
|
}, function() {
|
|
|
|
|
logger.info(`[${connectionId}]: write data[length = ${data.length}] to local connection`);
|
|
|
|
|
});
|
|
|
|
|
}
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
2017-04-06 09:21:19 +08:00
|
|
|
targetConnection.setKeepAlive(true, 5000);
|
2017-04-05 19:41:04 +08:00
|
|
|
targetConnection.on('end', function() {
|
|
|
|
|
connection.close();
|
|
|
|
|
});
|
|
|
|
|
targetConnection.on('error', function(error) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.error(`[${connectionId}]: an error of target connection occured`, error);
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_DESTROYED;
|
2017-04-05 19:41:04 +08:00
|
|
|
targetConnection.destroy();
|
|
|
|
|
connection.close();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (data.length > addressHeader.headerLen) {
|
2017-04-06 10:21:58 +08:00
|
|
|
dataCache.push(data.slice(addressHeader.headerLen));
|
2017-04-05 19:41:04 +08:00
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
2017-04-06 10:21:58 +08:00
|
|
|
case STAGE_CONNECTING:
|
|
|
|
|
dataCache.push(data);
|
|
|
|
|
break;
|
|
|
|
|
|
2017-04-05 19:41:04 +08:00
|
|
|
case STAGE_STREAM:
|
2017-04-06 10:21:58 +08:00
|
|
|
targetConnection.write(data, function() {
|
|
|
|
|
logger.info(`[${connectionId}]: write data[length = ${data.length}] to target connection`);
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
2017-04-06 10:21:58 +08:00
|
|
|
connection.on('ping', function() {
|
|
|
|
|
return connection.pong('', false, true);
|
|
|
|
|
});
|
2017-04-05 19:41:04 +08:00
|
|
|
connection.on('close', function(hadError) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: close event[had error = ${hadError}] of connection has been triggered`);
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
|
|
|
|
connection.on('error', function(error) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.error(`[${connectionId}]: an error of connection occured`, error);
|
2017-04-05 19:41:04 +08:00
|
|
|
connection.terminate();
|
|
|
|
|
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;
|
2017-04-06 10:21:58 +08:00
|
|
|
var serverConnection, cmd, addressHeader, ping;
|
2017-04-05 19:41:04 +08:00
|
|
|
|
|
|
|
|
var canWriteToLocalConnection = true;
|
2017-04-06 10:21:58 +08:00
|
|
|
var dataCache = [];
|
2017-04-05 19:41:04 +08:00
|
|
|
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: accept connection from client`);
|
2017-04-05 19:41:04 +08:00
|
|
|
connection.setKeepAlive(true, 10000);
|
|
|
|
|
connection.on('data', function(data) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: read data[length = ${data.length}] from client connection at stage[${STAGE[stage]}]`);
|
2017-04-05 19:41:04 +08:00
|
|
|
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) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.error('[${connectionId}]: only supports connect cmd');
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_DESTROYED;
|
2017-04-05 19:41:04 +08:00
|
|
|
return connection.end("\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00");
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: connecting to ${addressHeader.dstAddr}:${addressHeader.dstPort}`);
|
2017-04-05 19:41:04 +08:00
|
|
|
connection.write("\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00");
|
|
|
|
|
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_CONNECTING;
|
2017-04-05 19:41:04 +08:00
|
|
|
|
|
|
|
|
serverConnection = new WebSocket('ws://' + serverAddress + ':' + serverPort, {
|
2017-04-06 09:21:19 +08:00
|
|
|
perMessageDeflate: false
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
|
|
|
|
serverConnection.on('open', function() {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: connecting to websocket server`);
|
2017-04-05 19:41:04 +08:00
|
|
|
serverConnection.send(encryptor.encrypt(data.slice(3)), function() {
|
|
|
|
|
stage = STAGE_STREAM;
|
2017-04-06 10:21:58 +08:00
|
|
|
dataCache = Buffer.concat(dataCache);
|
|
|
|
|
serverConnection.send(encryptor.encrypt(dataCache), {
|
|
|
|
|
binary: true
|
|
|
|
|
}, function() {
|
|
|
|
|
logger.info(`[${connectionId}]: write data[length = ${dataCache.length}] to client connection`);
|
|
|
|
|
dataCache = null;
|
|
|
|
|
});
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
2017-04-06 10:21:58 +08:00
|
|
|
|
|
|
|
|
ping = setInterval(function() {
|
|
|
|
|
serverConnection.ping('', false, true);
|
|
|
|
|
}, 30000);
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
|
|
|
|
serverConnection.on('message', function(data) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: read data[length = ${data.length}] from websocket server connection`);
|
2017-04-05 19:41:04 +08:00
|
|
|
canWriteToLocalConnection && connection.write(encryptor.decrypt(data), function() {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: write data[length = ${data.length}] to client connection`);
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
serverConnection.on('error', function(error) {
|
2017-04-06 10:21:58 +08:00
|
|
|
ping && clearInterval(ping);
|
|
|
|
|
logger.error(`[${connectionId}]: an error of server connection occured`, error);
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_DESTROYED;
|
2017-04-05 19:41:04 +08:00
|
|
|
connection.end();
|
|
|
|
|
});
|
|
|
|
|
serverConnection.on('close', function() {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: server connection isclosed`);
|
|
|
|
|
ping && clearInterval(ping);
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_DESTROYED;
|
2017-04-05 19:41:04 +08:00
|
|
|
connection.end();
|
|
|
|
|
});
|
2017-04-06 10:21:58 +08:00
|
|
|
|
|
|
|
|
if (data.length > addressHeader.headerLen + 3) {
|
|
|
|
|
dataCache.push(data.slice(addressHeader.headerLen + 3));
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case STAGE_CONNECTING:
|
|
|
|
|
dataCache.push(data);
|
2017-04-05 19:41:04 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case STAGE_STREAM:
|
2017-04-06 09:21:19 +08:00
|
|
|
canWriteToLocalConnection && serverConnection.send(encryptor.encrypt(data), {
|
|
|
|
|
binary: true
|
|
|
|
|
}, function() {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: write data[length = ${data.length}] to websocket server connection`);
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
connection.on('end', function() {
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_DESTROYED;
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: end event of client connection has been triggered`);
|
2017-04-05 19:41:04 +08:00
|
|
|
});
|
|
|
|
|
connection.on('close', function(hadError) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.info(`[${connectionId}]: close event[had error = ${hadError}] of client connection has been triggered`);
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_DESTROYED;
|
2017-04-05 19:41:04 +08:00
|
|
|
canWriteToLocalConnection = false;
|
|
|
|
|
});
|
|
|
|
|
connection.on('error', function(error) {
|
2017-04-06 10:21:58 +08:00
|
|
|
logger.error(`[${connectionId}]: an error of client connection occured`, error);
|
2017-04-06 09:21:19 +08:00
|
|
|
stage = STAGE_DESTROYED;
|
2017-04-05 19:41:04 +08:00
|
|
|
connection.destroy();
|
|
|
|
|
canWriteToLocalConnection = false;
|
|
|
|
|
serverConnection && serverConnection.close();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports.TCPRelay = TCPRelay;
|