From 138f59bea3c16abfa7acdff1062feee619655a7f Mon Sep 17 00:00:00 2001 From: tigerbot Date: Thu, 26 Oct 2017 14:39:51 -0600 Subject: [PATCH] implemented proxying decrypted TLS streams in raw form --- lib/admin/config.js | 24 ++++++++---- lib/goldilocks.js | 94 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/lib/admin/config.js b/lib/admin/config.js index 2d10423..4e3c82f 100644 --- a/lib/admin/config.js +++ b/lib/admin/config.js @@ -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. diff --git a/lib/goldilocks.js b/lib/goldilocks.js index 8ad86b3..b3226e1 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -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))); }); }