made TCP binding and forwarding modules respond to config changes
This commit is contained in:
parent
c637671c78
commit
c132861cab
|
@ -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,31 +31,31 @@ 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 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) {
|
function checkTcpProxy(conn, opts) {
|
||||||
var proxied = false;
|
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
|
// 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.
|
// this is the decrypted side of a TLS connection we can't handle it here.
|
||||||
if (!opts.servername || !opts.encrypted) { return proxied; }
|
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) {
|
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,7 +146,8 @@ 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 (checkTcpProxy(conn, opts)) { return; }
|
if (checkTcpForward(conn, opts)) { return; }
|
||||||
|
if (checkTcpProxy(conn, opts)) { return; }
|
||||||
|
|
||||||
// XXX PEEK COMMENT XXX
|
// XXX PEEK COMMENT XXX
|
||||||
// TODO we can have our cake and eat it too
|
// 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 = 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;
|
|
||||||
});
|
|
||||||
|
|
||||||
(config.tcp.modules || []).forEach(function (mod) {
|
if (!Array.isArray(wanted)) { wanted = []; }
|
||||||
if (mod.type === 'forward') {
|
wanted = wanted.map(Number).filter((port) => port > 0 && port < 65356);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var portList = Object.keys(tcpPortMap).map(Number).sort();
|
var closeProms = current.filter(function (port) {
|
||||||
portList.forEach(function (port) {
|
return wanted.indexOf(port) < 0;
|
||||||
listenPromises.push(listeners.tcp.add(port, tcpHandler));
|
}).map(function (port) {
|
||||||
});
|
return listeners.close(port, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
if (!config.mdns.disabled) {
|
// We don't really need to filter here since listening on the same port with the
|
||||||
require('./mdns').start(deps, config, portList[0]);
|
// 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
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue