Files
shadowsocks-over-websocket/tcprelay.js

474 lines
13 KiB
JavaScript
Raw Normal View History

2017-04-05 19:41:04 +08:00
const net = require('net');
2017-04-07 16:04:38 +08:00
const path = require('path');
2017-04-05 19:41:04 +08:00
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'
};
2017-04-06 18:32:59 +08:00
const SERVER_STATUS_INIT = 0;
const SERVER_STATUS_RUNNING = 1;
const SERVER_STATUS_STOPPED = 2;
2017-04-05 19:41:04 +08:00
var globalConnectionId = 1;
2017-04-06 18:32:59 +08:00
var connections = {};
2017-04-05 19:41:04 +08:00
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
};
}
2017-04-07 16:04:38 +08:00
function TCPRelay(config, isLocal) {
2017-04-05 19:41:04 +08:00
this.isLocal = isLocal;
this.server = null;
2017-04-06 18:32:59 +08:00
this.status = SERVER_STATUS_INIT;
2017-04-05 19:41:04 +08:00
this.config = require('./config.json');
if (config) {
this.config = Object.assign(this.config, config);
}
2017-04-07 16:04:38 +08:00
this.logger = null;
this.logLevel = 'error';
this.logFile = null;
this.serverName = null;
2017-04-05 19:41:04 +08:00
}
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.getStatus = function() {
return this.status;
};
2017-04-05 19:41:04 +08:00
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.setServerName = function(serverName) {
this.serverName = serverName;
return this;
2017-04-05 19:41:04 +08:00
};
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.getServerName = function() {
if (!this.serverName) {
this.serverName = this.isLocal ? 'local' : 'server';
}
return this.serverName;
};
2017-04-05 19:41:04 +08:00
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.setLogLevel = function(logLevel) {
this.logLevel = logLevel;
return this;
2017-04-05 19:41:04 +08:00
};
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.getLogLevel = function() {
return this.logLevel;
};
2017-04-06 18:32:59 +08:00
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.setLogFile = function(logFile) {
if (logFile && !path.isAbsolute(logFile)) {
logFile = process.cwd() + '/' + logFile;
}
this.logFile = logFile;
return this;
};
2017-04-06 18:32:59 +08:00
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.getLogFile = function() {
return this.logFile;
2017-04-06 10:44:45 +08:00
};
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.initLogger = function() {
if (this.logFile) {
log4js.loadAppender('file');
log4js.addAppender(log4js.appenders.file(this.logFile), this.getServerName());
}
this.logger = log4js.getLogger(this.getServerName());
this.logger.setLevel(this.logLevel);
2017-04-06 18:32:59 +08:00
};
2017-04-07 16:04:38 +08:00
TCPRelay.prototype.initServer = function() {
2017-04-06 15:02:53 +08:00
var self = this;
2017-04-06 14:56:42 +08:00
return new Promise(function(resolve, reject) {
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);
});
2017-04-06 18:32:59 +08:00
server.on('close', function() {
self.logger.info('server is closed');
self.status = SERVER_STATUS_STOPPED;
});
2017-04-06 14:56:42 +08:00
server.listen(port, address);
} else {
server = self.server = new WebSocket.Server({
host: address,
port: port,
perMessageDeflate: false,
backlog: MAX_CONNECTIONS
});
server.on('connection', function(connection) {
return self.handleConnectionByServer(connection);
});
}
server.on('error', function(error) {
2017-04-07 16:04:38 +08:00
self.logger.fatal('an error of', self.getServerName(), 'occured', error);
2017-04-06 18:32:59 +08:00
self.status = SERVER_STATUS_STOPPED;
2017-04-06 14:56:42 +08:00
reject(error);
2017-04-05 19:41:04 +08:00
});
2017-04-06 14:56:42 +08:00
server.on('listening', function() {
self.logger.info(self.getServerName(), 'is listening on', address + ':' + port);
2017-04-06 18:32:59 +08:00
self.status = SERVER_STATUS_RUNNING;
2017-04-06 14:56:42 +08:00
resolve();
2017-04-05 19:41:04 +08:00
});
});
};
//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-06 18:32:59 +08:00
connections[connectionId] = connection;
2017-04-05 19:41:04 +08:00
connection.on('message', function(data) {
data = encryptor.decrypt(data);
2017-04-07 16:04:38 +08:00
logger.debug(`[${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() {
2017-04-07 16:04:38 +08:00
logger.debug(`[${connectionId}]: write data[length = ${dataCache.length}] to target connection`);
2017-04-06 10:21:58 +08:00
dataCache = null;
});
2017-04-05 19:41:04 +08:00
stage = STAGE_STREAM;
});
targetConnection.on('data', function(data) {
2017-04-07 16:04:38 +08:00
logger.debug(`[${connectionId}]: read data[length = ${data.length}] from target connection`);
2017-04-06 10:21:58 +08:00
if (connection.readyState == WebSocket.OPEN) {
connection.send(encryptor.encrypt(data), {
binary: true
}, function() {
2017-04-07 16:04:38 +08:00
logger.debug(`[${connectionId}]: write data[length = ${data.length}] to local connection`);
2017-04-06 10:21:58 +08:00
});
}
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() {
2017-04-07 16:04:38 +08:00
logger.debug(`[${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-06 18:32:59 +08:00
connections[connectionId] = null;
targetConnection && targetConnection.destroy();
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();
2017-04-06 18:32:59 +08:00
connections[connectionId] = null;
2017-04-05 19:41:04 +08:00
targetConnection && targetConnection.end();
});
2017-04-07 16:04:38 +08:00
};
2017-04-05 19:41:04 +08:00
//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-06 18:32:59 +08:00
connections[connectionId] = connection;
2017-04-05 19:41:04 +08:00
connection.setKeepAlive(true, 10000);
connection.on('data', function(data) {
2017-04-07 16:04:38 +08:00
logger.debug(`[${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() {
2017-04-07 16:04:38 +08:00
logger.debug(`[${connectionId}]: write data[length = ${dataCache.length}] to client connection`);
2017-04-06 10:21:58 +08:00
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-07 16:04:38 +08:00
logger.debug(`[${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-07 16:04:38 +08:00
logger.debug(`[${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-07 16:04:38 +08:00
logger.info(`[${connectionId}]: server connection is closed`);
2017-04-06 10:21:58 +08:00
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-07 16:04:38 +08:00
logger.debug(`[${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;
2017-04-06 18:32:59 +08:00
connections[connectionId] = null;
serverConnection && serverConnection.terminate();
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 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;
2017-04-06 18:32:59 +08:00
connections[connectionId] = null;
2017-04-05 19:41:04 +08:00
serverConnection && serverConnection.close();
});
2017-04-07 16:04:38 +08:00
};
TCPRelay.prototype.bootstrap = function() {
this.initLogger();
return this.initServer();
};
TCPRelay.prototype.stop = function() {
var self = this;
var connId = null;
return new Promise(function(resolve, reject) {
if (self.server) {
self.server.close(function() {
resolve();
});
for (connId in connections) {
if (connections[connId]) {
self.isLocal ? connections[connId].destroy() : connections[connId].terminate();
}
}
} else {
resolve();
}
});
};
2017-04-05 19:41:04 +08:00
module.exports.TCPRelay = TCPRelay;