vpn.email.server.gfw
Version:
Vpn.Email over firewall mode
287 lines (286 loc) • 12.2 kB
JavaScript
/*!
* Copyright 2017 Vpn.Email network security technology Canada Inc. All Rights Reserved.
*
* Vpn.Email network technolog Canada Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
;
const Net = require("net");
const Dns = require("dns");
const fs = require("fs");
const Async = require("async");
const Stream = require("stream");
const cluster = require("cluster");
const HttpProxy = require("./proxy/httpProxy");
const IptablesAdd = require("./util/iptablesAdd");
const Compress = require("./proxy/compress");
const StreamFun = require("./proxy/streamFunction");
const MaxAllowedTimeOut = 1000 * 60 * 5;
const saveLog = (_log, fileName) => {
const log = new Date().toString() + `: ${_log}\n`;
fs.appendFile(fileName, log, { encoding: 'utf8' }, err => {
console.log(log);
});
};
const otherRespon = (body, _status) => {
const Ranges = (_status === 200) ? 'Accept-Ranges: bytes\r\n' : '';
const Content = (_status === 200) ? `Content-Type: text/plain; charset=utf-8\r\n` : 'Content-Type: text/html\r\n';
const headers = `Server: nginx/1.6.2\r\n`
+ `Date: ${new Date().toUTCString()}\r\n`
+ Content
+ `Content-Length: ${body.length}\r\n`
+ `Connection: keep-alive\r\n`
+ `Vary: Accept-Encoding\r\n`
+ '\r\n';
const status = _status === 200 ? 'HTTP/1.1 200 OK\r\n' : 'HTTP/1.1 404 Not Found\r\n';
return status + headers + body;
};
const return404 = () => {
const kkk = '<html>\r\n<head><title>404 Not Found</title></head>\r\n<body bgcolor="white">\r\n<center><h1>404 Not Found</h1></center>\r\n<hr><center>nginx/1.6.2</center>\r\n</body>\r\n</html>\r\n';
return otherRespon(Buffer.from(kkk, 'utf8'), 404);
};
const dnsLookup = (hostName, CallBack) => {
return Dns.lookup(hostName, { all: true }, (err, data) => {
if (err)
return CallBack(err);
const _buf = Buffer.from(JSON.stringify(data), 'utf8');
return CallBack(null, _buf);
});
};
class listen extends Stream.Transform {
constructor(headString) {
super();
this.headString = headString;
}
_transform(chunk, encode, cb) {
console.log(this.headString);
console.log(chunk.toString('hex'));
console.log(this.headString);
return cb(null, chunk);
}
}
class ssModeV1 {
constructor(clientIp, port, password) {
this.clientIp = clientIp;
this.port = port;
this.password = password;
this.logFileName = logDir + this.clientIp;
this.serverNet = null;
IptablesAdd.appPort(port, (err, ret) => {
if (err) {
saveLog(`IptablesAdd.appPort ERROR: ${err.message}`, this.logFileName);
return process.exit(1);
}
this.serverNet = Net.createServer(socket => {
const _remoteAddress = socket.remoteAddress;
const remoteAddress = _remoteAddress.split(':').length > 2 ? _remoteAddress.split(':')[3] : _remoteAddress;
const id = `[${remoteAddress}]:[${socket.remotePort}]`;
const isAllowIP = remoteAddress === this.clientIp;
const streamFunBlock = new StreamFun.blockRequestData(isAllowIP, MaxAllowedTimeOut);
const streamDecrypt = new Compress.decryptStream(this.password);
const streamEncrypt = new Compress.encryptStream(this.password, 500, null, () => {
const firstConnect = new FirstConnect(socket, streamEncrypt);
firstConnect.on('error', err => {
console.log(`firstConnect.on ERROR:`, err.message);
return socket.end(return404());
});
socket.pipe(streamFunBlock).pipe(streamDecrypt).pipe(firstConnect);
});
streamFunBlock.on('error', err => {
console.log(`streamFunBlock.on ERROR:`, err.message);
if (/404/.test(err.message))
return socket.end(return404());
return socket.end();
});
socket.on('end', () => {
return this.serverNet.getConnections((err, count) => {
console.log(id, 'socket.on END! connected = ', count);
});
});
socket.on('unpipe', src => {
return socket.end();
});
socket.on('error', err => {
return saveLog('HTTP on ERROR:' + err.message, this.logFileName);
});
});
this.serverNet.on('error', err => {
return saveLog('SS mode Net on error:' + err.message, this.logFileName);
});
this.serverNet.listen(port, null, 512, () => {
const log = 'SS mode start up listening ' + `[${clientIp}:${port}]`;
console.log(log);
return saveLog(log, this.logFileName);
});
});
IptablesAdd.appPort(port + 1, (err, ret) => {
if (err) {
saveLog(`IptablesAdd.appPort ERROR: ${err.message}`, this.logFileName);
return process.exit(1);
}
const http1 = Net.createServer(socket => {
const _remoteAddress = socket.remoteAddress;
const remoteAddress = _remoteAddress.split(':').length > 2 ? _remoteAddress.split(':')[3] : _remoteAddress;
const id = `[${remoteAddress}]:[${socket.remotePort}]`;
socket.once('data', _buf => {
const keepRead = (data) => {
const header = new HttpProxy.httpProxy(data);
if (header._parts.length < 2) {
return socket.once('data', _Buf => {
const _data = Buffer.concat([data, _Buf]);
return keepRead(_data);
});
}
if (!header.isHttpRequest) {
return socket.end();
}
if (remoteAddress !== clientIp || !header.isGet) {
return socket.end(return404());
}
const url = header.Url.path.substr(1);
if (!url || !url.length) {
saveLog('HTTP.on data GET url null', this.logFileName);
return socket.end(return404());
}
const Data = Buffer.from(url, 'base64');
try {
const request = JSON.parse(Data.toString('utf8'));
return this.testConnect(id, request, socket);
}
catch (e) {
return console.log('test data JSON.parse catch ERROR:', e.message);
}
};
return keepRead(_buf);
});
socket.on('error', err => {
return saveLog('SS test mode Net on error:' + err.message, this.logFileName);
});
});
http1.on('error', err => {
return saveLog('SS test mode Net on error:' + err.message, this.logFileName);
});
http1.listen(port + 1, null, 512, () => {
const log = 'SS test mode start up listening ' + `[${clientIp}:${port + 1}]`;
console.log(log);
return saveLog(log, this.logFileName);
});
});
}
saveLog(log) {
saveLog(log, this.logFileName);
}
nslookupRequest(hostName, socket) {
return Async.waterfall([
next => dnsLookup(hostName, next),
(data1, next) => Compress.encrypt(Buffer.from(JSON.stringify(data1), 'utf8'), this.password, next)
], (err, result) => {
if (err) {
saveLog('nslookup error!' + err.message, this.logFileName);
return socket.end(return404());
}
const lll = result.toString('base64');
const time = new Date().getTime();
const _buf = otherRespon(lll, 200);
return socket.end(_buf, () => {
const _time = (new Date().getTime() - time) / 1000;
return saveLog(`nslookup to client [${lll.length}] byte speed:[${lll.length / _time}] byte/sec`, this.logFileName);
});
});
}
testConnect(id, data, socket) {
const conn = Net.createConnection(data.port, data.host, () => {
conn.pipe(socket).pipe(conn);
conn.write(Buffer.from(data.buffer, 'base64'));
});
conn.on('end', () => {
return console.log('test getWayRequest conn.on END');
});
return conn.on('error', err => {
return console.log('test getWayRequest conn.on error', err.message);
});
}
}
class FirstConnect extends Stream.Writable {
constructor(clientSocket, encrypt) {
super();
this.clientSocket = clientSocket;
this.encrypt = encrypt;
this.socket = null;
}
_write(chunk, encode, cb) {
if (!this.socket) {
const _data = chunk.toString('utf8');
try {
const data = JSON.parse(_data);
if (data.hostName && data.hostName.length) {
return dnsLookup(data.hostName, (err, data) => {
if (err) {
return cb(err);
}
this.encrypt.pipe(this.clientSocket);
this.encrypt.end(data);
});
}
if (data.uuid) {
return this.socket = Net.connect({ port: data.port, host: data.host }, () => {
this.socket.on('error', err => {
console.log('FirstConnect socket on error!', err.message);
this.end();
});
this.socket.pipe(this.encrypt).pipe(this.clientSocket);
this.socket.write(Buffer.from(data.buffer, 'base64'));
return cb();
});
}
return cb(new Error('unknow connect!'));
}
catch (e) {
return cb(e);
}
}
if (this.socket.writable) {
this.socket.write(chunk);
return cb();
}
return cb(new Error('FirstConnect socket.writable=false'));
}
}
const clientIp = process.argv[2];
const clientPort = process.argv[3];
const password = process.argv[4];
const logDir = '/var/log/vpn.email/';
const logSystem = logDir + 'syslog';
if (!clientIp || !clientPort || !password) {
console.log(`Usage: node server clientIP clientPort password!`);
process.exit(1);
}
fs.access(logDir, fs.constants.R_OK | fs.constants.W_OK, err => {
if (err) {
fs.mkdir(logDir, err1 => {
if (err1) {
console.log(`${new Date().toString()} vpn.email.server.gfw can't mkdir log path!`);
process.exit(1);
}
});
}
});
if (cluster.isMaster) {
let worker = cluster.fork();
worker.on('exit', () => {
worker = cluster.fork();
});
}
else {
new ssModeV1(clientIp, parseInt(clientPort), password);
}