implemented ability to proxy TLS based on SNI

This commit is contained in:
tigerbot 2017-05-08 17:59:45 -06:00
parent f32db19b52
commit 99a3de6496
1 changed files with 106 additions and 55 deletions

View File

@ -20,6 +20,22 @@ module.exports.create = function (deps, config) {
var tunnelAdminTlsOpts = {};
var tls = require('tls');
function domainMatches(pattern, servername) {
// Everything matches '*'
if (pattern === '*') {
return true;
}
if (/^\*./.test(pattern)) {
// get rid of the leading "*." to more easily check the servername against it
pattern = pattern.slice(2);
return pattern === servername.slice(-pattern.length);
}
// pattern doesn't contains any wildcards, so exact match is required
return pattern === servername;
}
var tcpRouter = {
_map: { }
, _create: function (address, port) {
@ -123,68 +139,103 @@ module.exports.create = function (deps, config) {
}
};
var tlsRouter = {
_map: { }
, _create: function (address, port/*, nextServer*/) {
// port provides hinting for https, smtps, etc
return function (socket, firstChunk, opts) {
if (opts.hyperPeek) {
// See "PEEK COMMENT" for more info
// This was peeked at properly, so we don't have to re-wrap it
// in order to get the connection to not hang.
// The real first 'data' and 'readable' events will occur as they should
program.tlsTunnelServer.emit('connection', socket);
return;
proxy: function (socket, opts, mod) {
var newConn = deps.net.createConnection({
port: mod.port
, host: mod.address || '127.0.0.1'
, servername: opts.servername
, data: opts.data
, remoteFamily: opts.family || socket.remoteFamily || socket._remoteFamily || socket._handle._parent.owner.stream.remoteFamily
, remoteAddress: opts.address || socket.remoteAddress || socket._remoteAddress || socket._handle._parent.owner.stream.remoteAddress
, remotePort: opts.port || socket.remotePort || socket._remotePort || socket._handle._parent.owner.stream.remotePort
}, function () {
// this will happen before 'data' is triggered
});
newConn.pipe(socket);
socket.pipe(newConn);
}
, terminate: function (socket) {
// We terminate the TLS by emitting the connections of the TLS server and it should handle
// everything we need to do for us.
program.tlsTunnelServer.emit('connection', socket);
}
, handleModules: function (socket, opts) {
// needs to wind up in one of 2 states:
// 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket)
// 2. Terminated (goes on to a particular module or route, including the admin interface)
var handled = (config.tls.modules || []).some(function (mod) {
var relevant = mod.domains.some(function (pattern) {
return domainMatches(pattern, opts.servername);
});
if (!relevant) {
return false;
}
var servername = opts.servername;
var packerStream = require('tunnel-packer').Stream;
var myDuplex = packerStream.create(socket);
if (mod.name === 'proxy') {
tlsRouter.proxy(socket, opts, mod);
}
else if (mod.name === 'terminate') {
tlsRouter.terminate(socket);
}
else {
console.error('saw unknown TLS module', mod);
return false;
}
myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress;
myDuplex.remotePort = opts.remotePort || myDuplex.remotePort;
console.log('[tlsRouter] ' + address + ':' + port + ' servername', servername, myDuplex.remoteAddress);
return true;
});
// needs to wind up in one of 3 states:
// 1. SNI-based Proxy / Tunnel (we don't even need to put it through the tlsSocket)
// 2. Admin Interface (skips the proxying)
// 3. Terminated (goes on to a particular module or route)
//myDuplex.__tlsTerminated = true;
process.nextTick(function () {
// this must happen after the socket is emitted to the next in the chain,
// but before any more data comes in via the network
socket.unshift(firstChunk);
});
// nextServer.emit could be used here
program.tlsTunnelServer.emit('connection', myDuplex);
// Why all this wacky-do with the myDuplex?
// because https://github.com/nodejs/node/issues/8854, that's why
// (because node's internal networking layer == 💩 sometimes)
socket.on('data', function (chunk) {
console.log('[' + Date.now() + '] tls socket data', chunk.byteLength);
myDuplex.push(chunk);
});
socket.on('error', function (err) {
console.error('[error] httpsTunnel (Admin) TODO close');
console.error(err);
myDuplex.emit('error', err);
});
socket.on('close', function () {
myDuplex.end();
});
};
// We gotta do something, so when in doubt terminate the TLS since we don't really have
// any good place to default to when proxying.
if (!handled) {
tlsRouter.terminate(socket);
}
}
, get: function getTcpRouter(address, port) {
address = address || '0.0.0.0';
var id = address + ':' + port;
if (!tlsRouter._map[id]) {
tlsRouter._map[id] = tlsRouter._create(address, port);
, processSocket: function (socket, firstChunk, opts) {
if (opts.hyperPeek) {
// See "PEEK COMMENT" for more info
// This was already peeked at by the tunneler and this connection has been created
// in a way that should work with node's TLS server, so we don't need to do any
// of the myDuplex stuff that we need to do with non-tunnel connections.
tlsRouter.handleModules(socket, opts);
return;
}
return tlsRouter._map[id];
// Why all this wacky-do with the myDuplex?
// because https://github.com/nodejs/node/issues/8854, that's why
// (because node's internal networking layer == 💩 sometimes)
var myDuplex = require('tunnel-packer').Stream.create(socket);
myDuplex.remoteAddress = opts.remoteAddress || myDuplex.remoteAddress;
myDuplex.remotePort = opts.remotePort || myDuplex.remotePort;
socket.on('data', function (chunk) {
console.log('[' + Date.now() + '] tls socket data', chunk.byteLength);
myDuplex.push(chunk);
});
socket.on('error', function (err) {
console.error('[error] httpsTunnel (Admin) TODO close');
console.error(err);
myDuplex.emit('error', err);
});
socket.on('close', function () {
myDuplex.end();
});
var address = opts.localAddress || socket.localAddress;
var port = opts.localPort || socket.localPort;
console.log('[tlsRouter] ' + address + ':' + port + ' servername', opts.servername, myDuplex.remoteAddress);
tlsRouter.handleModules(myDuplex, opts);
process.nextTick(function () {
// this must happen after the socket is emitted to the next in the chain,
// but before any more data comes in via the network
socket.unshift(firstChunk);
});
}
};
@ -197,7 +248,7 @@ module.exports.create = function (deps, config) {
if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) {
console.log('tryTls');
opts.servername = (parseSni(firstChunk)||'').toLowerCase() || 'localhost.invalid';
tlsRouter.get(opts.localAddress || conn.localAddress, conn.localPort)(conn, firstChunk, opts);
tlsRouter.processSocket(conn, firstChunk, opts);
return;
}