From c132861cab9bfcef8b417668817932fb928a5bbe Mon Sep 17 00:00:00 2001 From: tigerbot Date: Thu, 26 Oct 2017 18:43:51 -0600 Subject: [PATCH] made TCP binding and forwarding modules respond to config changes --- lib/goldilocks.js | 167 ++++++++++++++++++++++++++++------------------ lib/servers.js | 36 ++++++---- lib/worker.js | 2 +- 3 files changed, 125 insertions(+), 80 deletions(-) diff --git a/lib/goldilocks.js b/lib/goldilocks.js index ee54887..e54084c 100644 --- a/lib/goldilocks.js +++ b/lib/goldilocks.js @@ -5,7 +5,7 @@ module.exports.create = function (deps, config) { //var PromiseA = global.Promise; var PromiseA = require('bluebird'); - var listeners = require('./servers').listeners; + var listeners = require('./servers').listeners.tcp; var domainUtils = require('./domain-utils'); var modules; @@ -31,31 +31,31 @@ module.exports.create = function (deps, config) { modules.http = require('./modules/http').create(deps, config, modules.tls.middleware); } + function proxy(mod, conn, opts) { + // 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; + } + function checkTcpProxy(conn, opts) { var proxied = false; - // TCP Proxying (ie forwarding based on domain name not incoming port) only works for + // TCP Proxying (ie routing based on domain name [vs local 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; } @@ -63,7 +63,7 @@ module.exports.create = function (deps, config) { return dom.modules.tcp.some(function (mod) { if (mod.type !== 'proxy') { return false; } - return proxy(mod); + return proxy(mod, conn, opts); }); }); @@ -71,12 +71,24 @@ module.exports.create = function (deps, config) { if (mod.type !== 'proxy') { return false; } if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; } - return proxy(mod); + return proxy(mod, conn, opts); }); return proxied; } + function checkTcpForward(conn, opts) { + // TCP forwarding (ie routing connections based on local port) requires the local port + if (!conn.localPort) { return false; } + + return config.tcp.modules.some(function (mod) { + if (mod.type !== 'forward') { return false; } + if (mod.ports.indexOf(conn.localPort) < 0) { return false; } + + return proxy(mod, conn, opts); + }); + } + // opts = { servername, encrypted, peek, data, remoteAddress, remotePort } function peek(conn, firstChunk, opts) { if (!modules) { @@ -134,7 +146,8 @@ module.exports.create = function (deps, config) { console.log('[tcpHandler]', logName, 'connection closed', (Date.now()-start)/1000); }); - if (checkTcpProxy(conn, opts)) { return; } + if (checkTcpForward(conn, opts)) { return; } + if (checkTcpProxy(conn, opts)) { return; } // XXX PEEK COMMENT XXX // TODO we can have our cake and eat it too @@ -159,21 +172,6 @@ module.exports.create = function (deps, config) { }); } - function createTcpForwarder(mod) { - var dest = require('./domain-utils').separatePort(mod.address || ''); - dest.port = dest.port || mod.port; - dest.host = dest.host || mod.host || 'localhost'; - - return function (conn) { - var newConnOpts = {}; - addrProperties.forEach(function (name) { - newConnOpts['_'+name] = conn[name]; - }); - - deps.proxy(conn, Object.assign(newConnOpts, dest)); - }; - } - deps.tunnel = deps.tunnel || {}; deps.tunnel.net = { createConnection: function (opts, cb) { @@ -240,37 +238,76 @@ module.exports.create = function (deps, config) { deps.tunnelClients = require('./tunnel-client-manager').create(deps, config); deps.tunnelServer = require('./tunnel-server-manager').create(deps, config); - var listenPromises = []; - var tcpPortMap = {}; - config.tcp.bind.filter(Number).forEach(function (port) { - tcpPortMap[port] = true; - }); + function updateListeners() { + var current = listeners.list(); + var wanted = config.tcp.bind; - (config.tcp.modules || []).forEach(function (mod) { - if (mod.type === 'forward') { - var forwarder = createTcpForwarder(mod); - mod.ports.forEach(function (port) { - if (!tcpPortMap[port]) { - console.log("forwarding port", port, "that wasn't specified in bind"); - } else { - delete tcpPortMap[port]; - } - listenPromises.push(listeners.tcp.add(port, forwarder)); - }); - } - else if (mod.type !== 'proxy') { - console.warn('unknown TCP module specified', mod); - } - }); + if (!Array.isArray(wanted)) { wanted = []; } + wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356); - var portList = Object.keys(tcpPortMap).map(Number).sort(); - portList.forEach(function (port) { - listenPromises.push(listeners.tcp.add(port, tcpHandler)); - }); + var closeProms = current.filter(function (port) { + return wanted.indexOf(port) < 0; + }).map(function (port) { + return listeners.close(port, 1000); + }); - if (!config.mdns.disabled) { - require('./mdns').start(deps, config, portList[0]); + // We don't really need to filter here since listening on the same port with the + // same handler function twice is basically a no-op. + var openProms = wanted.map(function (port) { + return listeners.add(port, tcpHandler); + }); + + return Promise.all(closeProms.concat(openProms)); } - return PromiseA.all(listenPromises); + var mainPort; + function updateConf() { + updateListeners().catch(function (err) { + console.error('Error updating TCP listeners to match bind configuration'); + console.error(err); + }); + + var unforwarded = {}; + config.tcp.bind.forEach(function (port) { + unforwarded[port] = true; + }); + + config.tcp.modules.forEach(function (mod) { + if (['forward', 'proxy'].indexOf(mod.type) < 0) { + console.warn('unknown TCP module type specified', JSON.stringify(mod)); + } + if (mod.type !== 'forward') { return; } + + mod.ports.forEach(function (port) { + if (!unforwarded[port]) { + console.warn('trying to forward TCP port ' + port + ' multiple times or it is unbound'); + } else { + delete unforwarded[port]; + } + }); + }); + + // Not really sure what we can reasonably do to prevent this. At least not without making + // our configuration validation more complicated. + if (!Object.keys(unforwarded).length) { + console.warn('no bound TCP ports are not being forwarded, admin interface will be inaccessible'); + } + + // If we are listening on port 443 make that the main port we respond to mDNS queries with + // otherwise choose the lowest number port we are bound to but not forwarding. + if (unforwarded['443']) { + mainPort = 443; + } else { + mainPort = Object.keys(unforwarded).map(Number).sort((a, b) => a - b)[0]; + } + } + + updateConf(); + if (!config.mdns.disabled) { + require('./mdns').start(deps, config, mainPort); + } + + return { + updateConf + }; }; diff --git a/lib/servers.js b/lib/servers.js index 5d4aa05..8b3da88 100644 --- a/lib/servers.js +++ b/lib/servers.js @@ -10,20 +10,16 @@ module.exports.addTcpListener = function (port, handler) { if (stat) { if (stat._closing) { - module.exports.destroyTcpListener(port); - } - else if (handler !== stat.handler) { - - // we'll replace the current listener + stat.server.destroy(); + } else { + // We're already listening on the port, so we only have 2 options. We can either + // replace the handler or reject with an error. (Though neither is really needed + // if the handlers are the same). Until there is reason to do otherwise we are + // opting for the replacement. stat.handler = handler; resolve(); return; } - else { - // this exact listener is already open - resolve(); - return; - } } var enableDestroy = require('server-destroy'); @@ -34,7 +30,7 @@ module.exports.addTcpListener = function (port, handler) { stat = serversMap[port] = { server: server , handler: handler - , _closing: null + , _closing: false }; // Add .destroy so we can close all open connections. Better if added before listen @@ -66,14 +62,24 @@ module.exports.addTcpListener = function (port, handler) { }); }); }; -module.exports.closeTcpListener = function (port) { +module.exports.closeTcpListener = function (port, timeout) { return new PromiseA(function (resolve) { var stat = serversMap[port]; if (!stat) { resolve(); return; } - stat.server.once('close', resolve); + stat._closing = true; + + var timeoutId; + if (timeout) { + timeoutId = setTimeout(() => stat.server.destroy(), timeout); + } + + stat.server.once('close', function () { + clearTimeout(timeoutId); + resolve(); + }); stat.server.close(); }); }; @@ -84,7 +90,9 @@ module.exports.destroyTcpListener = function (port) { } }; module.exports.listTcpListeners = function () { - return Object.keys(serversMap).map(Number).filter(Boolean); + return Object.keys(serversMap).map(Number).filter(function (port) { + return port && !serversMap[port]._closing; + }); }; diff --git a/lib/worker.js b/lib/worker.js index 435c066..6779244 100644 --- a/lib/worker.js +++ b/lib/worker.js @@ -52,10 +52,10 @@ function create(conf) { , socks5: require('./socks5-server').create(deps, conf) , ddns: require('./ddns').create(deps, conf) , udp: require('./udp').create(deps, conf) + , tcp: require('./goldilocks').create(deps, conf) }; Object.assign(deps, modules); - require('./goldilocks.js').create(deps, conf); process.removeListener('message', create); process.on('message', update); }