Allows to default config of the Tor instances to be specified at startup and during runtime. Allows for the configuration of individual Tor instances to be specified at startup and during runtime

This commit is contained in:
Zachary Boyd 2018-05-09 18:15:58 -07:00
parent b3546799da
commit 369ffa5d4f
11 changed files with 254 additions and 44 deletions

View file

@ -8,6 +8,8 @@ EXPOSE 53
EXPOSE 9077
ENV PARENT_DATA_DIRECTORTY /var/lib/tor-router
ENV PATH $PATH:/app/bin
ADD https://deb.nodesource.com/setup_8.x /tmp/nodejs_install

View file

@ -24,6 +24,7 @@ The following command line switches and their environment variable equivalents a
|Command line switch|Environment Variable|Description|
|-------------------|--------------------|-----------|
|-f, --config | |Path to a JSON configuration file to use|
|-c, --controlPort |CONTROL_PORT |Port the control server will bind to (see below)|
|-j, --instances |INSTANCES |Number of Tor instances to spawn|
|-s, --socksPort |SOCKS_PORT |Port the SOCKS proxy will bind to|
@ -31,9 +32,52 @@ The following command line switches and their environment variable equivalents a
|-h, --httpPort |HTTP_PORT |Port the HTTP proxy will bind to|
|-l, --logLevel |LOG_LEVEL |Log level (defaults to "info") set to "null" to disable logging. To see a log of all network traffic set logLevel to "verbose"|
For example: `tor-router -j 3 -s 9050` would start the proxy with 3 tor instances and listen for SOCKS connections on 9050.
## Configuration
Using the `--config` or `-f` command line switch you can set the path to a JSON file which can be used to load configuration on startup
The same variable names from the command line switches are used to name the keys in the JSON file.
Example:
```
{
"controlPort": 9077,
"logLevel": "debug"
}
```
Using the configuration file you can set a default configuration for all Tor instances
```
{
"torConfig": {
"MaxCircuitDirtiness": "10"
}
}
```
You can also specify a configuration for individual instances by setting the "instances" field to an array instead of an integer
```
{
"instances": [
{
"Config": {
"DataDirectory": "/tmp/my-tor-dir1"
}
},
{
"Config": {
"DataDirectory": "/tmp/my-tor-dir2"
}
}
]
}
```
## Control Server
A JSON-RPC 2 TCP Server will listen on port 9077 by default. Using the rpc server the client can add/remove Tor instances and get a new identity (which includes a new ip address) while Tor Router is running.

View file

@ -9,6 +9,7 @@ var DNSServer = TorRouter.DNSServer;
var TorPool = TorRouter.TorPool;
var ControlServer = TorRouter.ControlServer;
var winston = require('winston');
var async = require('async');
process.title = 'tor-router';
@ -56,10 +57,15 @@ let argv_config = require('yargs')
describe: 'Controls the verbosity of console log output. Default level is "info". Set to "verbose" to see all network traffic logged or "null" to disable logging completely [default: info]',
demand: false
// ,default: "info"
},
p: {
alias: 'parentDataDirectory',
describe: 'Parent directory that will contain the data directories for the instances',
demand: false
}
});
let env_whitelist = [ "CONTROL_PORT", "INSTANCES", "SOCKS_PORT", "DNS_PORT", "HTTP_PORT", "LOG_LEVEL", "controlPort", "instances", "socksPort", "dnsPort", "httpPort", "logLevel" ];
let env_whitelist = [ "CONTROL_PORT", "INSTANCES", "SOCKS_PORT", "DNS_PORT", "HTTP_PORT", "LOG_LEVEL", 'PARENT_DATA_DIRECTORIES', "controlPort", "instances", "socksPort", "dnsPort", "httpPort", "logLevel", 'parentDataDirectories' ];
nconf
.env({
@ -110,7 +116,7 @@ let http_port = nconf.get('httpPort');
let control_port = nconf.get('controlPort');
let control = new ControlServer(logger);
let control = new ControlServer(logger, nconf);
process.on('SIGHUP', () => {
control.torPool.new_ips();
@ -129,7 +135,7 @@ if (dns_port) {
}
if (instances) {
logger && logger.info(`[tor]: starting ${instances} tor instances...`)
logger && logger.info(`[tor]: starting ${Array.isArray(instances) ? instances.length : instances} tor instances...`)
control.torPool.create(instances, (err) => {
logger && logger.info('[tor]: tor started');
});
@ -138,3 +144,14 @@ if (instances) {
control.listen(control_port, () => {
logger && logger.info(`[control]: Control Server listening on ${control_port}`);
})
function cleanUp(error) {
async.each(control.torPool.instances, (instance, next) => {
instance.exit(next);
}, (exitError) => {
process.exit(Number(Boolean(error || exitError)));
});
}
process.on('exit', cleanUp);
process.on('SIGINT', cleanUp);
process.on('uncaughtException', cleanUp);

91
package-lock.json generated
View file

@ -29,6 +29,19 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
"integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
},
"array-union": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
"integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
"requires": {
"array-uniq": "1.0.3"
}
},
"array-uniq": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
"integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY="
},
"asn1": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz",
@ -314,6 +327,19 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"del": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz",
"integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=",
"requires": {
"globby": "6.1.0",
"is-path-cwd": "1.0.0",
"is-path-in-cwd": "1.0.1",
"p-map": "1.2.0",
"pify": "3.0.0",
"rimraf": "2.2.8"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@ -500,6 +526,25 @@
"path-is-absolute": "1.0.1"
}
},
"globby": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=",
"requires": {
"array-union": "1.0.2",
"glob": "7.1.2",
"object-assign": "4.1.1",
"pify": "2.3.0",
"pinkie-promise": "2.0.1"
},
"dependencies": {
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
"integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
}
}
},
"graceful-readlink": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
@ -636,6 +681,27 @@
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
"integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
},
"is-path-cwd": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
"integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0="
},
"is-path-in-cwd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
"integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
"requires": {
"is-path-inside": "1.0.1"
}
},
"is-path-inside": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
"integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
"requires": {
"path-is-inside": "1.0.2"
}
},
"is-stream": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
@ -986,6 +1052,11 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"nanoid": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.0.2.tgz",
"integrity": "sha512-sCTwJt690lduNHyqknXJp8pRwzm80neOLGaiTHU2KUJZFVSErl778NNCIivEQCX5gNT0xR1Jy3HEMe/TABT6lw=="
},
"native-dns": {
"version": "git+https://github.com/znetstar/node-dns.git#336f1d3027b2a3da719b5cd65380219267901aeb",
"requires": {
@ -5493,6 +5564,11 @@
"integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=",
"dev": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -5542,6 +5618,11 @@
"p-limit": "1.2.0"
}
},
"p-map": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz",
"integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA=="
},
"p-try": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
@ -5557,6 +5638,11 @@
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
"integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM="
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
@ -5568,6 +5654,11 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=",
"dev": true
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
},
"pinkie": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",

View file

@ -21,10 +21,12 @@
},
"dependencies": {
"async": "^2.1.4",
"del": "^3.0.0",
"eventemitter2": "^3.0.0",
"get-port": "^2.1.0",
"jrpc2": "^1.0.5",
"lodash": "^4.17.4",
"nanoid": "^1.0.2",
"native-dns": "git+https://github.com/znetstar/node-dns.git",
"nconf": "^0.10.0",
"socks-proxy-agent": "^3.0.1",

View file

@ -5,8 +5,8 @@ const HTTPServer = require('./HTTPServer');
const rpc = require('jrpc2');
class ControlServer {
constructor(logger) {
this.torPool = new TorPool(null, null, logger);
constructor(logger, nconf) {
this.torPool = new TorPool(null, null, logger, nconf);
this.logger = logger;
let server = this.server = new rpc.Server();
@ -33,6 +33,15 @@ class ControlServer {
});
}).bind(this) );
server.expose('addInstances', (function (instances) {
return new Promise((resolve, reject) => {
this.torPool.add(instances, (error, instances) => {
if (error) reject(error);
else resolve();
});
});
}).bind(this) );
server.expose('removeInstances', (function (instances) {
return new Promise((resolve, reject) => {
this.torPool.remove(instances, (error) => {
@ -42,11 +51,25 @@ class ControlServer {
});
}).bind(this) );
server.expose('removeInstancesAt', (function (instance_index) {
return new Promise((resolve, reject) => {
this.torPool.remove_at(instance_index, (error) => {
if (error) reject(error);
else resolve();
});
});
}).bind(this) );
server.expose('newIps', (function() {
this.torPool.new_ips();
return Promise.resolve();
}).bind(this) );
server.expose('newIpAt', (function(index) {
this.torPool.new_ip_at(index);
return Promise.resolve();
}).bind(this) );
server.expose('nextInstance', (function () {
this.torPool.next();
return Promise.resolve();
@ -70,13 +93,12 @@ class ControlServer {
}
createTorPool(options) {
this.torPool = new TorPool(null, options, this.logger);
this.torPool;
this.torPool = new TorPool(null, options, this.logger, this.nconf);
return Promise.resolve();
}
createSOCKSServer(port) {
this.socksServer = new SOCKSServer(this.torPool, this.logger);
this.socksServer = new SOCKSServer(this.torPool, this.logger, this.nconf);
this.socksServer.listen(port || 9050);
this.logger && this.logger.info(`[socks]: Listening on ${port}`);
this.socksServer;
@ -84,7 +106,7 @@ class ControlServer {
}
createHTTPServer(port) {
this.httpServer = new HTTPServer(this.torPool, this.logger);
this.httpServer = new HTTPServer(this.torPool, this.logger, this.nconf);
this.httpServer.listen(port || 9080);
this.logger && this.logger.info(`[http]: Listening on ${port}`);
this.httpServer;
@ -92,7 +114,7 @@ class ControlServer {
}
createDNSServer(port) {
this.dnsServer = new DNSServer(this.torPool, this.logger);
this.dnsServer = new DNSServer(this.torPool, this.logger, this.nconf);
this.dnsServer.serve(port || 9053);
this.logger && this.logger.info(`[dns]: Listening on ${port}`);
this.dnsServer;

View file

@ -2,8 +2,8 @@ const dns = require('native-dns');
const UDPServer = require('native-dns').UDPServer;
class DNSServer extends UDPServer {
constructor(tor_pool, logger, options, timeout) {
super(options || {});
constructor(tor_pool, logger, nconf) {
super(this.nconf.get('dns:options'));
this.logger = logger;
this.tor_pool = tor_pool;
@ -14,7 +14,7 @@ class DNSServer extends UDPServer {
let outbound_req = dns.Request({
question,
server: { address: '127.0.0.1', port: dns_port, type: 'udp' },
timeout: this.timeout
timeout: this.nconf.get('dns:timeout')
});
outbound_req.on('message', (err, answer) => {

View file

@ -6,7 +6,7 @@ const URL = require('url');
const SocksProxyAgent = require('socks-proxy-agent');
class HTTPServer extends Server {
constructor(tor_pool, logger) {
constructor(tor_pool, logger, nconf) {
let handle_http_connections = (req, res) => {
let d = domain.create();
d.on('error', (error) => {
@ -154,6 +154,7 @@ class HTTPServer extends Server {
this.on('connect', handle_connect_connections);
this.logger = logger;
this.nconf = nconf;
this.tor_pool = tor_pool;
}
};

View file

@ -3,7 +3,7 @@ const SOCKS5Server = socks.Server;
const domain = require('domain');
class SOCKSServer extends SOCKS5Server{
constructor(tor_pool, logger) {
constructor(tor_pool, logger, nconf) {
let handleConnection = (info, accept, deny) => {
let d = domain.create();
@ -79,6 +79,7 @@ class SOCKSServer extends SOCKS5Server{
super(handleConnection);
this.logger = logger;
this.nconf = nconf;
this.useAuth(socks.auth.None());
}

View file

@ -20,30 +20,37 @@ Array.prototype.rotate = (function() {
const EventEmitter = require('eventemitter2').EventEmitter2;
const async = require('async');
const TorProcess = require('./TorProcess');
const temp = require('temp');
const _ = require('lodash');
temp.track();
const path = require('path');
const nanoid = require('nanoid');
const fs = require('fs');
class TorPool extends EventEmitter {
constructor(tor_path, config, logger) {
constructor(tor_path, default_config, logger, nconf) {
super();
config = config || {};
this.tor_config = config;
default_config = _.extend({}, (default_config || {}), nconf.get('torConfig'));
this.default_tor_config = default_config;
this.data_directory = nconf.get('parentDataDirectory');
!fs.existsSync(this.data_directory) && fs.mkdirSync(this.data_directory);
this.tor_path = tor_path || 'tor';
this._instances = [];
this.logger = logger;
this.nconf = nconf;
}
get instances() {
return this._instances.filter((tor) => tor.ready).slice(0);
}
create_instance(callback) {
let config = _.extend({}, this.tor_config)
let instance = new TorProcess(this.tor_path, config, this.logger);
create_instance(instance_definition, callback) {
instance_definition.Config = instance_definition.Config || {};
instance_definition.Config = _.extend(instance_definition.Config, this.default_tor_config);
let instance_id = nanoid();
instance_definition.Config.DataDirectory = instance_definition.Config.DataDirectory || path.join(this.data_directory, instance_id);
let instance = new TorProcess(this.tor_path, instance_definition.Config, this.logger, this.nconf);
instance.id = instance_id;
instance.definition = instance_definition;
instance.create((error) => {
if (error) return callback(error);
this._instances.push(instance);
@ -56,13 +63,23 @@ class TorPool extends EventEmitter {
});
}
create(instances, callback) {
if (!Number(instances)) return callback(null, []);
async.map(Array.from(Array(Number(instances))), (nothing, next) => {
this.create_instance(next);
add(instance_definitions, callback) {
async.each(instance_definitions, (instance_definition, next) => {
this.create_instance(instance_definition, next);
}, (callback || (() => {})));
}
create(instances, callback) {
if (typeof(instances) === 'number') {
instances = Array.from(Array(instances)).map(() => {
return {
Config: {}
};
});
}
return this.add(instances, callback);
}
remove(instances, callback) {
let instances_to_remove = this._instances.splice(0, instances);
async.each(instances_to_remove, (instance, next) => {
@ -70,18 +87,33 @@ class TorPool extends EventEmitter {
}, callback);
}
remove_at(instance_index, callback) {
let instance = this._instances.slice(instance_index, 1);
instance.exit(() => {
callback();
});
}
next() {
this._instances = this._instances.rotate(1);
return this.instances[0];
}
exit() {
this.instances.forEach((tor) => tor.exit());
exit(callback) {
async.each(this.instances, (instance,next) => {
instance.exit(next);
}, (error) => {
callback && callback(error);
});
}
new_ips() {
this.instances.forEach((tor) => tor.new_ip());
}
new_ip_at(index) {
this.instances[index].new_ip();
}
};
module.exports = TorPool;

View file

@ -1,33 +1,31 @@
const spawn = require('child_process').spawn;
const _ = require('lodash');
const temp = require('temp');
const async = require('async');
const fs = require('fs');
const getPort = require('get-port');
const del = require('del');
const EventEmitter = require('eventemitter2').EventEmitter2;
const temp = require('temp');
temp.track();
class TorProcess extends EventEmitter {
constructor(tor_path, config, logger) {
constructor(tor_path, config, logger, nconf) {
super();
this.tor_path = tor_path || 'tor';
this.nconf = nconf;
this.logger = logger;
this.tor_config = _.extend({
Log: 'notice stdout',
DataDirectory: temp.mkdirSync(),
NewCircuitPeriod: '10'
}, (config || { }));
config.DataDirectory = config.DataDirectory || temp.mkdirSync();
this.tor_config = config;
}
exit(callback) {
this.process.once('exit', (code) => {
callback && callback(null, code);
del(this.tor_config.DataDirectory).then(() => callback && callback(null, code)).catch((error) => callback && callback(error, code));
});
this.process.kill('SIGKILL');
}
new_ip() {