made TCP binding and forwarding modules respond to config changes

This commit is contained in:
tigerbot 2017-10-26 18:43:51 -06:00
parent c637671c78
commit c132861cab
3 changed files with 125 additions and 80 deletions

View File

@ -5,7 +5,7 @@ module.exports.create = function (deps, config) {
//var PromiseA = global.Promise; //var PromiseA = global.Promise;
var PromiseA = require('bluebird'); var PromiseA = require('bluebird');
var listeners = require('./servers').listeners; var listeners = require('./servers').listeners.tcp;
var domainUtils = require('./domain-utils'); var domainUtils = require('./domain-utils');
var modules; var modules;
@ -31,15 +31,7 @@ module.exports.create = function (deps, config) {
modules.http = require('./modules/http').create(deps, config, modules.tls.middleware); modules.http = require('./modules/http').create(deps, config, modules.tls.middleware);
} }
function checkTcpProxy(conn, opts) { function proxy(mod, 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 // First thing we need to add to the connection options is where to proxy the connection to
var newConnOpts = domainUtils.separatePort(mod.address || ''); var newConnOpts = domainUtils.separatePort(mod.address || '');
newConnOpts.port = newConnOpts.port || mod.port; newConnOpts.port = newConnOpts.port || mod.port;
@ -56,6 +48,14 @@ module.exports.create = function (deps, config) {
return true; return true;
} }
function checkTcpProxy(conn, opts) {
var proxied = false;
// 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; }
proxied = config.domains.some(function (dom) { proxied = config.domains.some(function (dom) {
if (!dom.modules || !Array.isArray(dom.modules.tcp)) { return false; } if (!dom.modules || !Array.isArray(dom.modules.tcp)) { return false; }
if (!nameMatchesDomains(opts.servername, dom.names)) { 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) { return dom.modules.tcp.some(function (mod) {
if (mod.type !== 'proxy') { return false; } 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 (mod.type !== 'proxy') { return false; }
if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; } if (!nameMatchesDomains(opts.servername, mod.domains)) { return false; }
return proxy(mod); return proxy(mod, conn, opts);
}); });
return proxied; 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 } // opts = { servername, encrypted, peek, data, remoteAddress, remotePort }
function peek(conn, firstChunk, opts) { function peek(conn, firstChunk, opts) {
if (!modules) { if (!modules) {
@ -134,6 +146,7 @@ module.exports.create = function (deps, config) {
console.log('[tcpHandler]', logName, 'connection closed', (Date.now()-start)/1000); console.log('[tcpHandler]', logName, 'connection closed', (Date.now()-start)/1000);
}); });
if (checkTcpForward(conn, opts)) { return; }
if (checkTcpProxy(conn, opts)) { return; } if (checkTcpProxy(conn, opts)) { return; }
// XXX PEEK COMMENT XXX // XXX PEEK COMMENT XXX
@ -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 = deps.tunnel || {};
deps.tunnel.net = { deps.tunnel.net = {
createConnection: function (opts, cb) { createConnection: function (opts, cb) {
@ -240,37 +238,76 @@ module.exports.create = function (deps, config) {
deps.tunnelClients = require('./tunnel-client-manager').create(deps, config); deps.tunnelClients = require('./tunnel-client-manager').create(deps, config);
deps.tunnelServer = require('./tunnel-server-manager').create(deps, config); deps.tunnelServer = require('./tunnel-server-manager').create(deps, config);
var listenPromises = []; function updateListeners() {
var tcpPortMap = {}; var current = listeners.list();
config.tcp.bind.filter(Number).forEach(function (port) { var wanted = config.tcp.bind;
tcpPortMap[port] = true;
if (!Array.isArray(wanted)) { wanted = []; }
wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356);
var closeProms = current.filter(function (port) {
return wanted.indexOf(port) < 0;
}).map(function (port) {
return listeners.close(port, 1000);
}); });
(config.tcp.modules || []).forEach(function (mod) { // We don't really need to filter here since listening on the same port with the
if (mod.type === 'forward') { // same handler function twice is basically a no-op.
var forwarder = createTcpForwarder(mod); var openProms = wanted.map(function (port) {
return listeners.add(port, tcpHandler);
});
return Promise.all(closeProms.concat(openProms));
}
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) { mod.ports.forEach(function (port) {
if (!tcpPortMap[port]) { if (!unforwarded[port]) {
console.log("forwarding port", port, "that wasn't specified in bind"); console.warn('trying to forward TCP port ' + port + ' multiple times or it is unbound');
} else { } else {
delete tcpPortMap[port]; delete unforwarded[port];
} }
listenPromises.push(listeners.tcp.add(port, forwarder));
}); });
}
else if (mod.type !== 'proxy') {
console.warn('unknown TCP module specified', mod);
}
}); });
var portList = Object.keys(tcpPortMap).map(Number).sort(); // Not really sure what we can reasonably do to prevent this. At least not without making
portList.forEach(function (port) { // our configuration validation more complicated.
listenPromises.push(listeners.tcp.add(port, tcpHandler)); 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) { if (!config.mdns.disabled) {
require('./mdns').start(deps, config, portList[0]); require('./mdns').start(deps, config, mainPort);
} }
return PromiseA.all(listenPromises); return {
updateConf
};
}; };

View File

@ -10,20 +10,16 @@ module.exports.addTcpListener = function (port, handler) {
if (stat) { if (stat) {
if (stat._closing) { if (stat._closing) {
module.exports.destroyTcpListener(port); stat.server.destroy();
} } else {
else if (handler !== stat.handler) { // 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
// we'll replace the current listener // if the handlers are the same). Until there is reason to do otherwise we are
// opting for the replacement.
stat.handler = handler; stat.handler = handler;
resolve(); resolve();
return; return;
} }
else {
// this exact listener is already open
resolve();
return;
}
} }
var enableDestroy = require('server-destroy'); var enableDestroy = require('server-destroy');
@ -34,7 +30,7 @@ module.exports.addTcpListener = function (port, handler) {
stat = serversMap[port] = { stat = serversMap[port] = {
server: server server: server
, handler: handler , handler: handler
, _closing: null , _closing: false
}; };
// Add .destroy so we can close all open connections. Better if added before listen // 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) { return new PromiseA(function (resolve) {
var stat = serversMap[port]; var stat = serversMap[port];
if (!stat) { if (!stat) {
resolve(); resolve();
return; 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(); stat.server.close();
}); });
}; };
@ -84,7 +90,9 @@ module.exports.destroyTcpListener = function (port) {
} }
}; };
module.exports.listTcpListeners = function () { 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;
});
}; };

View File

@ -52,10 +52,10 @@ function create(conf) {
, socks5: require('./socks5-server').create(deps, conf) , socks5: require('./socks5-server').create(deps, conf)
, ddns: require('./ddns').create(deps, conf) , ddns: require('./ddns').create(deps, conf)
, udp: require('./udp').create(deps, conf) , udp: require('./udp').create(deps, conf)
, tcp: require('./goldilocks').create(deps, conf)
}; };
Object.assign(deps, modules); Object.assign(deps, modules);
require('./goldilocks.js').create(deps, conf);
process.removeListener('message', create); process.removeListener('message', create);
process.on('message', update); process.on('message', update);
} }