implemented proxying decrypted TLS streams in raw form

This commit is contained in:
tigerbot 2017-10-26 14:39:51 -06:00
parent 0ef845f2d5
commit 138f59bea3
2 changed files with 95 additions and 23 deletions

View File

@ -73,6 +73,14 @@ Object.keys(moduleSchemas).forEach(function (name) {
validator.addSchema(schema, schema.id);
});
function addDomainRequirement(itemSchema) {
var result = Object.assign({}, itemSchema);
result.required = (result.required || []).concat('domains');
result.properties = Object.assign({}, result.properties);
result.properties.domains = { type: 'array', items: { type: 'string' }, minLength: 1};
return result;
}
function toSchemaRef(name) {
return { '$ref': '/modules/'+name };
}
@ -84,12 +92,11 @@ var moduleRefs = {
, ddns: [ 'dns@oauth3.org' ].map(toSchemaRef)
};
function addDomainRequirement(itemSchema) {
itemSchema.required = (itemSchema.required || []).concat('domains');
itemSchema.properties = itemSchema.properties || {};
itemSchema.properties.domains = { type: 'array', items: { type: 'string' }, minLength: 1};
return itemSchema;
}
// TCP is a bit special in that it has a module that doesn't operate based on domain name
// (ie forward), and a modules that does (ie proxy). It therefore has different module
// when part of the `domains` config, and when not part of the `domains` config the proxy
// modules must have the `domains` property while forward should not have it.
moduleRefs.tcp.push(addDomainRequirement(toSchemaRef('proxy')));
var domainSchema = {
type: 'array'
@ -104,6 +111,7 @@ var domainSchema = {
tls: { type: 'array', items: { oneOf: moduleRefs.tls }}
, http: { type: 'array', items: { oneOf: moduleRefs.http }}
, ddns: { type: 'array', items: { oneOf: moduleRefs.ddns }}
, tcp: { type: 'array', items: { oneOf: ['proxy'].map(toSchemaRef)}}
}
, additionalProperties: false
}
@ -185,7 +193,7 @@ var ddnsSchema = {
, token_id: { type: 'string'}
}
}
, modules: { type: 'array', items: { oneOf: moduleRefs.ddns }}
, modules: { type: 'array', items: addDomainRequirement({ oneOf: moduleRefs.ddns })}
}
};
var socks5Schema = {
@ -293,6 +301,7 @@ class DomainList extends IdList {
http: new ModuleList((dom.modules || {}).http)
, tls: new ModuleList((dom.modules || {}).tls)
, ddns: new ModuleList((dom.modules || {}).ddns)
, tcp: new ModuleList((dom.modules || {}).tcp)
};
});
}
@ -309,6 +318,7 @@ class DomainList extends IdList {
http: new ModuleList()
, tls: new ModuleList()
, ddns: new ModuleList()
, tcp: new ModuleList()
};
// We add these after instead of in the constructor to run the validation and manipulation
// in the ModList add function since these are all new modules.

View File

@ -6,13 +6,75 @@ module.exports.create = function (deps, config) {
//var PromiseA = global.Promise;
var PromiseA = require('bluebird');
var listeners = require('./servers').listeners;
var domainUtils = require('./domain-utils');
var modules;
var addrProperties = [
'remoteAddress'
, 'remotePort'
, 'remoteFamily'
, 'localAddress'
, 'localPort'
, 'localFamily'
];
function nameMatchesDomains(name, domainList) {
return domainList.some(function (pattern) {
return domainUtils.match(pattern, name);
});
}
function loadModules() {
modules = {};
modules.tls = require('./modules/tls').create(deps, config, netHandler);
modules.http = require('./modules/http.js').create(deps, config, modules.tls.middleware);
modules.tls = require('./modules/tls').create(deps, config, tcpHandler);
modules.http = require('./modules/http').create(deps, config, modules.tls.middleware);
}
function checkTcpProxy(conn, opts) {
var proxied = false;
// TCP Proxying (ie forwarding based on domain name not incoming port) only works for
// TLS wrapped connections, so if the opts don't give us a servername or don't tell us
// this is the decrypted side of a TLS connection we can't handle it here.
if (!opts.servername || !opts.encrypted) { return proxied; }
function proxy(mod) {
// First thing we need to add to the connection options is where to proxy the connection to
var newConnOpts = domainUtils.separatePort(mod.address || '');
newConnOpts.port = newConnOpts.port || mod.port;
newConnOpts.host = newConnOpts.host || mod.host || 'localhost';
// Then we add all of the connection address information. We need to prefix all of the
// properties with '_' so we can provide the information to any connection `createConnection`
// implementation but not have the default implementation try to bind the same local port.
addrProperties.forEach(function (name) {
newConnOpts['_' + name] = opts[name] || opts['_'+name] || conn[name] || conn['_'+name];
});
deps.proxy(conn, newConnOpts);
return true;
}
proxied = config.domains.some(function (dom) {
if (!dom.modules || !Array.isArray(dom.modules.tcp)) { return false; }
if (!nameMatchesDomains(opts.servername, dom.names)) { return false; }
return dom.modules.tcp.some(function (mod) {
if (mod.type !== 'proxy') { return false; }
return proxy(mod);
});
});
proxied = proxied || config.tcp.modules.some(function (mod) {
if (mod.type !== 'proxy') { return false; }
if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; }
return proxy(mod);
});
return proxied;
}
// opts = { servername, encrypted, peek, data, remoteAddress, remotePort }
@ -52,26 +114,28 @@ module.exports.create = function (deps, config) {
console.warn('failed to identify protocol from first chunk', firstChunk);
conn.destroy();
}
function netHandler(conn, opts) {
function tcpHandler(conn, opts) {
function getProp(name) {
return opts[name] || opts['_'+name] || conn[name] || conn['_'+name];
}
opts = opts || {};
var logName = getProp('remoteAddress') + ':' + getProp('remotePort') + ' -> ' +
getProp('localAddress') + ':' + getProp('localPort');
console.log('[netHandler]', logName, 'encrypted: '+opts.encrypted);
console.log('[tcpHandler]', logName, 'connection started - encrypted: ' + (opts.encrypted || false));
var start = Date.now();
conn.on('timeout', function () {
console.log('[netHandler]', logName, 'connection timed out', (Date.now()-start)/1000);
console.log('[tcpHandler]', logName, 'connection timed out', (Date.now()-start)/1000);
});
conn.on('end', function () {
console.log('[netHandler]', logName, 'connection ended', (Date.now()-start)/1000);
console.log('[tcpHandler]', logName, 'connection ended', (Date.now()-start)/1000);
});
conn.on('close', function () {
console.log('[netHandler]', logName, 'connection closed', (Date.now()-start)/1000);
console.log('[tcpHandler]', logName, 'connection closed', (Date.now()-start)/1000);
});
if (checkTcpProxy(conn, opts)) { return; }
// XXX PEEK COMMENT XXX
// TODO we can have our cake and eat it too
// we can skip the need to wrap the TLS connection twice
@ -95,7 +159,7 @@ module.exports.create = function (deps, config) {
});
}
function dnsListener(port, msg) {
function udpHandler(port, msg) {
if (!Array.isArray(config.udp.modules)) {
return;
}
@ -123,10 +187,8 @@ module.exports.create = function (deps, config) {
return function (conn) {
var newConnOpts = {};
['remote', 'local'].forEach(function (end) {
['Family', 'Address', 'Port'].forEach(function (name) {
newConnOpts['_'+end+name] = conn[end+name];
});
addrProperties.forEach(function (name) {
newConnOpts['_'+name] = conn[name];
});
deps.proxy(conn, Object.assign(newConnOpts, dest));
@ -176,7 +238,7 @@ module.exports.create = function (deps, config) {
} catch(e) {
}
netHandler(reader, wrapOpts);
tcpHandler(reader, wrapOpts);
process.nextTick(function () {
// this cb will cause the stream to emit its (actually) first data event
@ -217,19 +279,19 @@ module.exports.create = function (deps, config) {
listenPromises.push(listeners.tcp.add(port, forwarder));
});
}
else {
else if (mod.type !== 'proxy') {
console.warn('unknown TCP module specified', mod);
}
});
var portList = Object.keys(tcpPortMap).map(Number).sort();
portList.forEach(function (port) {
listenPromises.push(listeners.tcp.add(port, netHandler));
listenPromises.push(listeners.tcp.add(port, tcpHandler));
});
if (config.udp.bind) {
config.udp.bind.forEach(function (port) {
listenPromises.push(listeners.udp.add(port, dnsListener.bind(port)));
listenPromises.push(listeners.udp.add(port, udpHandler.bind(port)));
});
}