Move most of the README to the wiki. Add grunt and jsdoc and documentation for TorProcess

This commit is contained in:
Zachary Boyd 2018-09-13 15:00:51 -04:00
parent c539d47e11
commit 4aec5c3d0f
10 changed files with 1126 additions and 379 deletions

View file

@ -6,4 +6,5 @@ docker-compose.yml
.env
README.md
.vscode
.DS_Store
.DS_Store
doc

3
.gitignore vendored
View file

@ -2,4 +2,5 @@ node_modules
npm-debug.log
.env
.vscode
.DS_Store
.DS_Store
doc

View file

@ -1,5 +1,14 @@
# Changelog
## [4.0.2] - 2018-09-13
### Added
- Adds API documentation. To generate run `npm run doc` and open under `doc/index.html`
### Changed
- Much of the README has been moved to [the wiki](https://github.com/znetstar/tor-router/wiki)
- Updates granax to 3.1.4 which fixes a bug on MacOS
## [4.0.1] - 2018-09-11
## [4.0.0] - 2018-09-09

View file

@ -2,12 +2,6 @@ FROM node:10-jessie
WORKDIR /app
EXPOSE 9050
EXPOSE 9053
EXPOSE 9077
ENV PARENT_DATA_DIRECTORTY /var/lib/tor-router
ENV TOR_PATH /usr/bin/tor
@ -32,6 +26,12 @@ ADD . /app
ENV HOME /home/tor_router
EXPOSE 9050
EXPOSE 9053
EXPOSE 9077
ENTRYPOINT [ "tor-router" ]
CMD [ "-s", "-d", "-j", "1" ]

21
Gruntfile.js Normal file
View file

@ -0,0 +1,21 @@
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
jsdoc : {
dist : {
src: ['src/*.js', 'test/*.js', 'README.md'],
options: {
destination : 'doc',
template : "node_modules/ink-docstrap/template",
configure : "node_modules/ink-docstrap/template/jsdoc.conf.json"
}
}
}
});
grunt.loadNpmTasks('grunt-jsdoc');
grunt.registerTask('default', []);
grunt.registerTask('doc', ['jsdoc']);
};

155
README.md
View file

@ -2,11 +2,9 @@
[![NPM](https://nodei.co/npm/tor-router.png)](https://nodei.co/npm/tor-router/)
*Tor Router* is a simple SOCKS5 proxy server for distributing traffic across multiple instances of Tor. At startup Tor Router will run an arbitrary number of instances Tor and each request will be sent to a different instance in round-robin fashion. This can be used to increase anonymity, because each request will be sent on a different circuit and will most likely use a different exit-node, and also to increase performance since outbound traffic is now split across several instances of Tor.
*Tor Router* is a SOCKS5, DNS and HTTP proxy server for distributing traffic across multiple instances of Tor. At startup Tor Router will run an arbitrary number of instances Tor and each request will be sent to a different instance in round-robin fashion. This can be used to increase anonymity, because each request will be sent on a different circuit and will most likely use a different exit-node, and also to increase performance since outbound traffic is now split across several instances of Tor.
Tor Router also includes a DNS proxy server and a HTTP proxy as well, which like the SOCKS proxy will distribute traffic across multiple instances of Tor in round-robin fashion. The HTTP proxy server can be used to access Tor via an HTTP Proxy.
A list of changes can be [found here](https://github.com/znetstar/tor-router/blob/master/CHANGELOG.md)
A list of changes can be [found here](https://github.com/znetstar/tor-router/blob/master/CHANGELOG.md).
## Building and Running
@ -47,147 +45,12 @@ A full list of all available configuration options and their defaults can be fou
For example: `tor-router -j 3 -s 127.0.0.1:9050` would start the proxy with 3 tor instances and listen for SOCKS connections on localhost:9050.
## Documentation
For detailed examples and insturctions on using Tor Router [see the wiki](https://github.com/znetstar/tor-router/wiki).
To generate API documentation run `npm run doc`. The documentation will be available in `doc/`.
## Testing
Tests are written in mocha and can be found under `test/` and can be run with `npm test`
## 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 can optionally be assigned name and a weight. If the `loadBalanceMethod` config variable is set to "weighted" the weight field will determine how frequently the instance is used. If the instance is assigned a name the data directory will be preserved when the process is killed saving time when Tor is restarted. They can also be assigned one or more groups.
```
{
"loadBalanceMethod": "weighted",
"instances": [
{
"Name": "instance-1"
"Weight": 10,
"Config": {
}
},
{
"Name": "instance-2",
"Weight": 5,
"Config": {
}
}
]
}
```
If the `proxyByName` (argument `-n`) configuration property is set to "individual" or true you can use the instance name to send requests to a specific instance. The username field in the proxy URL will identify the instance. For example, using `http://instance-1:@localhost:9080` when connecting to the HTTP Proxy would route requests to "instance-1".
This feature works on the HTTP Proxy as well as the SOCKS Proxy, but not the DNS proxy since DNS lacks authentication.
## Groups
Instances can be assigned one or more groups.
```
{
"instances": [
{
"Name": "instance-1",
"Group": [ "foo", "bar ]
},
{
"Name": "instance-2",
"Group": [ "foo" ]
},
{
"Name": "instance-3",
"Group": "baz"
}
]
}
```
If the `proxyByName` (argument `-n`) configuration property is set to "group" requests can be routed to a group of instances using the username field in the proxy URL. For example, using `http://foo:@localhost:9080` as the proxy URL will send requests too "instance-1" and "instance-2" rotating them in a round-robin fashion.
## 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. The control server will also accept websocket connections if the `--websocketControlHost` or `-w` flag is set.
Example (in node):
```
const net = require('net');
let client = net.createConnection({ port: 9077 }, () => {
let rpcRequest = {
"method": "createInstances",
"params": [3],
"jsonrpc":"2.0",
"id": 1
};
client.write(JSON.stringify(rpcRequest));
});
client.on('data', (chunk) => {
let rawResponse = chunk.toString('utf8');
let rpcResponse = JSON.parse(rawResponse);
console.log(rpcResponse)
if (rpcResponse.id === 1) {
console.log('Three instances have been created!')
}
})
```
A full list of available RPC Methods can be [found here](https://github.com/znetstar/tor-router/blob/master/docs/rpc-methods.md)
## Tor Control Protocol
You can retrieve or set the configuration of instances while they're running via the Tor Control Protocol.
The example below will change the "MaxCircuitDirtiness" value for the first instance in the pool
Example:
```
let rpcRequest = {
"method": "setInstanceConfigAt",
"params": [0, "MaxCircuitDirtiness", "20"],
"jsonrpc":"2.0",
"id": 1
};
client.write(JSON.stringify(rpcRequest));
```
You can also send signals directly to instances or to all instances in the pool via the control protocol. A list of all signals can be [found here](https://gitweb.torproject.org/torspec.git/tree/control-spec.txt)
The example below will set the log level of all instances to "debug".
Example:
```
let rpcRequest = {
"method": "signalAllInstances",
"params": ["DEBUG"],
"jsonrpc":"2.0",
"id": 1
};
client.write(JSON.stringify(rpcRequest));
```
Tests are written in mocha and can be found under `test/` and can be run with `npm test`

View file

@ -1,186 +0,0 @@
# RPC Functions
The following functions are available via the RPC
## queryInstances()
Returns an array containing information on the instances currently running under the router.
## queryInstanceByName(instance_name: String)
Returns information on an instance identified by name
## queryInstancesByGroup(instance_name: String)
Returns an array containing information on the instances within a group.
## queryInstanceAt(instance_index: Integer)
Returns information on an instance identified by index.
## queryInstanceNames()
Returns a list of all instance names.
## queryGroupNames()
Returns a list of all instance groups.
## addInstanceToGroupByName(group: String, instance_name: String)
Adds an instance, identified by name, to a group
## addInstanceToGroupAt(group: String, instance_index: Integer)
Adds an instance, identified by index, to a group
## removeInstanceFromGroupByName(group: String, instance_name: String)
Removes an instance, identified by name, from a group
## removeInstanceFromGroupAt(group: String, instance_index: Integer)
Removes an instance, identified by index, from a group
## createInstances(instances: Array or Integer)
If passed an Integer, creates that many Tor instances. An array can also be passed describing the names, weights and configurations of prospective instances. :
```
var rpcRequest = {
"method": "createInstances",
"params": [
{
"Config": {
},
"Name": "instance-1",
"Weight": 10
},
...
],
"jsonrpc":"2.0",
"id": 1
};
```
Will wait until the Tor Instance has fully connected to the network before returning
## addInstances(instances: Array)
Serves the same purpose as "createInstances" but only takes an Array
## removeInstances(instances: Integer)
Removes a number of instances
## removeInstanceAt(instance_index: Integer)
Remove a specific instance from the pool by its index
## removeInstanceByName(instance_name: String)
Remove a specific instance from the pool by its name
## newIdentites()
Get new identites for all instances
## newIdentityAt(instance_index: Integer)
Get a new identity for a specific instance by its index
## newIdentityByName(instance_name: String)
Get a new identity for a specific instance by its name
## newIdentitiesByGroup(group: String)
Get new identities for all instances in a group
## nextInstance()
Cycle to the next instance using the load balancing method
## closeInstances()
Shutdown all Tor instances
## setTorConfig(config: Object)
Apples the provided configuration to all instances using the control protocol. Changes will be applied immediately.
## setTorConfigByGroup(group: String, config: Object)
Apples the provided configuration to all instances in a group using the control protocol. Changes will be applied immediately.
## getConfig(key: String)
Retrieve a value from application configuration.
## setConfig(key: String, value: Any)
Sets a value in application configuration.
## saveConfig()
If the application was started using a config file will save the current configuration.
## loadConfig()
If the application was started using a config file will load the configuration from the config file.
## getLoadBalanceMethod()
Get the current load balance method
## setLoadBalanceMethod(load_balance_method: String)
Set the current load balance method
## getInstanceConfigAt(instance_index: Integer, keyword: String)
Retrieves the current value of an option set in the configuration by the index of the instance using the control protocol.
Example:
The following would retrieve the path to the data directory of the instance
```
var rpcRequest = {
"method": "getInstanceConfigAt",
"params": [0, "DataDirectory"],
"jsonrpc":"2.0",
"id": 1
};
```
## getInstanceConfigByName(name: String, keyword: String)
Works the same way as `getInstanceConfigAt` except takes an instance name instead of an index
## setInstanceConfigAt(index: Integer, keyword: String, value: String)
Sets the value in the configuration of an instance using the control protocol. Changes will be applied immediately.
## setInstanceConfigByName(index: Integer, keyword: String, value: String)
Works the same way as `setInstanceConfigAt` except takes an instance name instead of an index
## signalAllInstances(signal: String)
Sends a signal using the control protocol to all instances
A list of all signals can be [found here](https://gitweb.torproject.org/torspec.git/tree/control-spec.txt)
## signalInstancesByGroup(group: String, signal: String)
Sends a signal using the control protocol to all instances in a group
## signalInstanceAt(index: Integer, signal: String)
Sends a signal using the control protocol to an instance identified by its index
## signalInstanceByName(name: String, signal: String)
Sends a signal using the control protocol to an instance identified by its name

915
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "tor-router",
"version": "4.0.1",
"version": "4.0.2",
"main": "src/index.js",
"repository": "git@github.com:znetstar/tor-router.git",
"author": "Zachary Boyd <zachary@zacharyboyd.nyc>",
@ -12,10 +12,16 @@
"scripts": {
"start": "bin/tor-router -s -d -j 1",
"test": "mocha -u tdd --exit test/index.js",
"debug": "node --inspect-brk bin/tor-router"
"debug": "node --inspect-brk bin/tor-router",
"build": "grunt",
"doc": "grunt doc"
},
"devDependencies": {
"chai": "^4.1.2",
"grunt": "^1.0.3",
"grunt-jsdoc": "^2.3.0",
"ink-docstrap": "^1.3.2",
"jsdoc": "^3.5.5",
"mocha": "^5.2.0",
"request": "^2.79.0",
"request-promise": "^4.2.2"
@ -25,7 +31,7 @@
"del": "^3.0.0",
"eventemitter3": "^3.1.0",
"get-port": "^2.1.0",
"granax": "^3.1.3",
"granax": "^3.1.4",
"jrpc2": "git+https://github.com/znetstar/jrpc2.git#f1521bd3f2fa73d716e74bf8f746d08d5e03e7d7",
"js-weighted-list": "^0.1.1",
"lodash": "^4.17.4",

View file

@ -1,5 +1,6 @@
const spawn = require('child_process').spawn;
const fs = require('fs');
const crypto = require('crypto');
const { connect } = require('net');
const os = require('os');
@ -14,17 +15,31 @@ const del = require('del');
const temp = require('temp');
const { TorController } = require('granax');
const nanoid = require("nanoid");
const winston = require('winston');
winston.
Promise.promisifyAll(temp);
Promise.promisifyAll(fs);
temp.track();
/**
* Class that represents an individual Tor process.
* @extends EventEmitter
*/
class TorProcess extends EventEmitter {
/**
* Creates a TorProcess Object
*
* @param {String} tor_path - Path to the Tor executable.
* @param {Object} [definition={}] - Object containing various options for the instance. See {@link https://github.com/znetstar/tor-router/wiki/Configuration|the wiki} for more info
* @param {Object} [granax_options] - Object containing options that will be passed to granax
* @param {Logger} [logger] - A winston logger used for logging.
*/
constructor(tor_path, definition, granax_options, logger) {
super();
this.logger = logger || require('./winston-silent-logger');
definition = definition || {};
definition.Group = definition.Group ? [].concat(definition.Group) : [];
this._definition = definition;
@ -37,6 +52,12 @@ class TorProcess extends EventEmitter {
this.tor_config.DataDirectory = this.tor_config.DataDirectory || temp.mkdirSync();
}
/**
* Kills the Tor process
*
* @async
* @returns {Promise}
*/
async exit() {
let p = new Promise((resolve, reject) => {
this.once('process_exit', (code) => {
@ -49,46 +70,119 @@ class TorProcess extends EventEmitter {
await p;
}
/**
* The unique identifier assigned to each instance
*
* @readonly
* @returns {String}
*/
get id() { return this._id; }
/**
* Groups the instance are currently in
*
* @readonly
* @returns {String[]}
*/
get instance_group() {
return (this.definition && this.definition.Group) || null;
return (this.definition && this.definition.Group) || [];
}
/**
* Either the "Name" property of the definition or the "id" property
*
* @readonly
* @returns {String}
*/
get instance_name() {
return (this.definition && this.definition.Name) || this.id;
}
/**
* The definition used to create the instance
*
* @readonly
* @returns {String}
*/
get definition() { return this._definition; }
/**
* The configuration passed to Tor. The same value as "definition.Config"
*
* @readonly
* @returns {Object}
*/
get tor_config() { return this.definition.Config; }
/**
* Port Tor is bound to for DNS traffic
*
* @readonly
* @returns {Number}
*/
get dns_port() {
return this._dns_port || null;
return this._ports.dns_port;
}
/**
* Port Tor is bound to for SOCKS5 traffic
*
* @readonly
* @returns {Number}
*/
get socks_port() {
return this._socks_port || null;
return this._ports.socks_port;
}
/**
* Port Tor is bound to for API access
*
* @readonly
* @returns {Number}
*/
get control_port() {
return this._control_port || null;
return this._ports.control_port;
}
/**
* Instance of granax connected to the Tor process
*
* @readonly
* @returns {Object}
*/
get controller() {
return this._controller || null;
return this._controller;
}
/**
* Property identifiyng whether Tor has started
*
* @readonly
* @returns {Boolean}
*/
get ready() { return this._ready; }
/* Passthrough to granax */
/**
* Requests a new identity via the control interface
*
* @async
*/
async new_identity() {
this.logger.info(`[tor-${this.instance_name}]: requested a new identity`);
await this.controller.cleanCircuitsAsync();
}
/**
* Retrieves a configuration value from the instance via the control interface
*
* @async
* @param {String} keyword - The name of the configuration property to retrieve
*
* @returns {Promise<String[]>}
*/
async get_config(keyword) {
if (!this.controller)
throw new Error(`Controller is not connected`);
@ -96,6 +190,15 @@ class TorProcess extends EventEmitter {
return await this.controller.getConfigAsync(keyword);
}
/**
* Sets a configuration value for the instance via the control interface
*
* @async
* @param {String} keyword - The name of the configuration property to retrieve
* @param value - Value to set the property to
*
* @returns {Promise}
*/
async set_config(keyword, value) {
if (!this.controller) {
return new Error(`Controller is not connected`);
@ -104,6 +207,14 @@ class TorProcess extends EventEmitter {
return await this.controller.setConfigAsync(keyword, value);
}
/**
* Sends a signal via the control tnterface
*
* @async
* @param {String} signal - The signal to send
*
* @returns {Promise}
*/
async signal(signal) {
if (!this.controller) {
throw new Error(`Controller is not connected`);
@ -112,10 +223,19 @@ class TorProcess extends EventEmitter {
return await this.controller.signal(signal);
}
/**
* Creates the Tor process based on the configuration provided. Promise is resolved when the process has been started
*
* @async
*
* @returns {Promise<ChildProcess>} - The process that has been created
*/
async create() {
let dnsPort = this._dns_port = await getPort();
let socksPort = this._socks_port = await getPort();
let controlPort = this._control_port = await getPort();
this._ports = {};
let dnsPort = this._ports.dns_port = await getPort();
let socksPort = this._ports.socks_port = await getPort();
let controlPort = this._ports.control_port = await getPort();
Object.freeze(this._ports);
let options = {
DNSPort: `127.0.0.1:${dnsPort}`,
@ -136,12 +256,20 @@ class TorProcess extends EventEmitter {
detached: false
});
tor.on('close', (code) => {
this.emit('process_exit', code);
tor.on('close', (async (code) => {
if (this.definition && !this.definition.Name) {
del(this.tor_config.DataDirectory, { force: true });
await del(this.tor_config.DataDirectory, { force: true });
}
});
/**
* An event that fires when the process has closed
*
* @event TorProcess#process_exit
* @type {Number}
* @returns {Number} - The exit code from the process
*/
this.emit('process_exit', code);
}));
tor.stderr.on('data', (data) => {
let error_message = Buffer.from(data).toString('utf8');
@ -151,6 +279,7 @@ class TorProcess extends EventEmitter {
this.once('ready', () => {
this._ready = true;
this.logger.info(`[tor-${this.instance_name}]: tor is ready`);
});
@ -167,6 +296,11 @@ class TorProcess extends EventEmitter {
} else {
this.logger.debug(`[tor-${this.instance_name}]: authenticated with tor instance via the control port`);
this.control_port_connected = true;
/**
* An event that fires when a connection has been established to the control interface
*
* @event TorProcess#controller_ready
*/
this.emit('controller_ready');
}
});
@ -175,28 +309,55 @@ class TorProcess extends EventEmitter {
tor.stdout.on('data', (data) => {
let text = Buffer.from(data).toString('utf8');
let msg = text.split('] ').pop();
let msg = text.indexOf('] ') !== -1 ? text.split('] ').pop() : null;
if (text.indexOf('Bootstrapped 100%: Done') !== -1){
this.bootstrapped = true;
/**
* An event that fires when the Tor process is fully bootstrapped (and ready for traffic)
*
* @event TorProcess#ready
*/
this.emit('ready');
}
if (text.indexOf('Opening Control listener on') !== -1) {
this.control_port_listening = true;
/**
* An event that fires when the Tor process has started listening for control interface traffic
*
* @event TorProcess#control_listen
*/
this.emit('control_listen');
}
if (text.indexOf('Opening Socks listener on') !== -1) {
this.socks_port_listening = true;
/**
* An event that fires when the Tor process has started listening for SOCKS5 traffic
*
* @event TorProcess#socks_listen
*/
this.emit('socks_listen');
}
if (text.indexOf('Opening DNS listener on') !== -1) {
/**
* An event that fires when the Tor process has started listening for DNS traffic
*
* @event TorProcess#dns_listen
*/
this.dns_port_listening = true;
this.emit('dns_listen');
}
if (text.indexOf('[err]') !== -1) {
/**
* An event that fires the Tor process has written an error to stdout or stderr or an error occured connecting to the control interface
*
* @event TorProcess#error
* @type {Error}
* @returns {Error}
*/
this.emit('error', new Error(msg));
this.logger.error(`[tor-${this.instance_name}]: ${msg}`);
}