improved feedback for bad TLS/TCP gateways

This commit is contained in:
tigerbot 2017-05-11 16:42:14 -06:00
parent e24f9412dd
commit 5777a885a4
4 changed files with 113 additions and 50 deletions

View File

@ -81,6 +81,7 @@ module.exports.create = function (deps, config) {
function createTcpForwarder(mod) { function createTcpForwarder(mod) {
var destination = mod.address.split(':'); var destination = mod.address.split(':');
var connected = false;
return function (conn) { return function (conn) {
var newConn = deps.net.createConnection({ var newConn = deps.net.createConnection({
@ -91,22 +92,28 @@ module.exports.create = function (deps, config) {
, remoteAddress: conn.remoteAddress , remoteAddress: conn.remoteAddress
, remotePort: conn.remotePort , remotePort: conn.remotePort
}, function () { }, function () {
connected = true;
newConn.pipe(conn);
conn.pipe(newConn);
}); });
// Not sure how to effectively report this to the user or client, but we need to listen // Not sure how to effectively report this to the user or client, but we need to listen
// for the event to prevent it from crashing us. // for the event to prevent it from crashing us.
newConn.on('error', function (err) { newConn.on('error', function (err) {
console.error('TCP forward connection error', err); if (connected) {
console.error('TCP forward remote error', err);
conn.end(); conn.end();
} else {
console.log('TCP forward connection error', err);
require('./proxy-err-resp').sendBadGateway(conn, err, config.debug);
}
}); });
conn.on('error', function (err) { conn.on('error', function (err) {
console.error('TCP forward client error', err); console.error('TCP forward client error', err);
newConn.end(); newConn.end();
}); });
newConn.pipe(conn);
conn.pipe(newConn);
}; };
} }

View File

@ -69,14 +69,10 @@ module.exports.create = function (deps, conf, greenlockMiddleware) {
proxy.on('error', function (err, req, res) { proxy.on('error', function (err, req, res) {
console.log(err); console.log(err);
res.writeHead(502); res.statusCode = 502;
if (err.code === 'ECONNREFUSED') { res.setHeader('Content-Type', 'text/html');
res.end('The connection was refused. Most likely the service being connected to ' res.setHeader('Connection', 'close');
+ 'has stopped running or the configuration is wrong.'); res.end(require('../proxy-err-resp').getRespBody(err, conf.debug));
}
else {
res.end('Bad Gateway: ' + err.code);
}
}); });
return function (req, res, next) { return function (req, res, next) {

View File

@ -4,6 +4,7 @@ module.exports.create = function (deps, config, netHandler) {
var tls = require('tls'); var tls = require('tls');
var parseSni = require('sni'); var parseSni = require('sni');
var greenlock = require('greenlock'); var greenlock = require('greenlock');
var localhostCerts = require('localhost.daplie.me-certificates');
var domainMatches = require('../match-domain').match; var domainMatches = require('../match-domain').match;
function extractSocketProp(socket, propName) { function extractSocketProp(socket, propName) {
@ -14,6 +15,34 @@ module.exports.create = function (deps, config, netHandler) {
; ;
} }
function wrapSocket(socket, opts) {
var myDuplex = require('tunnel-packer').Stream.create(socket);
myDuplex.remoteFamily = opts.remoteFamily || myDuplex.remoteFamily;
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();
});
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(opts.firstChunk);
});
return myDuplex;
}
var le = greenlock.create({ var le = greenlock.create({
// server: 'staging' // server: 'staging'
server: 'https://acme-v01.api.letsencrypt.org/directory' server: 'https://acme-v01.api.letsencrypt.org/directory'
@ -105,7 +134,7 @@ module.exports.create = function (deps, config, netHandler) {
if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) { if (/.*localhost.*\.daplie\.me/.test(sni.toLowerCase())) {
// TODO implement // TODO implement
if (!secureContexts[sni]) { if (!secureContexts[sni]) {
tlsOptions = require('localhost.daplie.me-certificates').mergeTlsOptions(sni, {}); tlsOptions = localhostCerts.mergeTlsOptions(sni, {});
} }
if (tlsOptions) { if (tlsOptions) {
secureContexts[sni] = tls.createSecureContext(tlsOptions); secureContexts[sni] = tls.createSecureContext(tlsOptions);
@ -120,7 +149,7 @@ module.exports.create = function (deps, config, netHandler) {
le.tlsOptions.SNICallback(sni, cb); le.tlsOptions.SNICallback(sni, cb);
}; };
var terminator = tls.createServer(terminatorOpts, function (socket) { var terminateServer = tls.createServer(terminatorOpts, function (socket) {
console.log('(pre-terminated) tls connection, addr:', socket.remoteAddress); console.log('(pre-terminated) tls connection, addr:', socket.remoteAddress);
netHandler(socket, { netHandler(socket, {
@ -135,6 +164,7 @@ module.exports.create = function (deps, config, netHandler) {
function proxy(socket, opts, mod) { function proxy(socket, opts, mod) {
var destination = mod.address.split(':'); var destination = mod.address.split(':');
var connected = false;
var newConn = deps.net.createConnection({ var newConn = deps.net.createConnection({
port: destination[1] port: destination[1]
@ -145,62 +175,60 @@ module.exports.create = function (deps, config, netHandler) {
, remoteFamily: opts.family || extractSocketProp(socket, 'remoteFamily') , remoteFamily: opts.family || extractSocketProp(socket, 'remoteFamily')
, remoteAddress: opts.address || extractSocketProp(socket, 'remoteAddress') , remoteAddress: opts.address || extractSocketProp(socket, 'remoteAddress')
, remotePort: opts.port || extractSocketProp(socket, 'remotePort') , remotePort: opts.port || extractSocketProp(socket, 'remotePort')
}, function () {
connected = true;
if (!opts.hyperPeek) {
newConn.write(opts.firstChunk);
}
newConn.pipe(socket);
socket.pipe(newConn);
}); });
// Not sure how to effectively report this to the user or client, but we need to listen // Not sure how to effectively report this to the user or client, but we need to listen
// for the event to prevent it from crashing us. // for the event to prevent it from crashing us.
newConn.on('error', function (err) { newConn.on('error', function (err) {
console.error('TLS proxy connection error', err); if (connected) {
console.error('TLS proxy remote error', err);
socket.end(); socket.end();
} else {
console.log('TLS proxy connection error', err);
var tlsOpts = localhostCerts.mergeTlsOptions('localhost.daplie.me', {isServer: true});
var decrypted;
if (opts.hyperPeek) {
decrypted = new tls.TLSSocket(socket, tlsOpts);
} else {
decrypted = new tls.TLSSocket(wrapSocket(socket, opts), tlsOpts);
}
require('../proxy-err-resp').sendBadGateway(decrypted, err, config.debug);
}
}); });
socket.on('error', function (err) { socket.on('error', function (err) {
console.error('TLS proxy client error', err); console.error('TLS proxy client error', err);
newConn.end(); newConn.end();
}); });
newConn.write(opts.firstChunk);
newConn.pipe(socket);
socket.pipe(newConn);
} }
function terminate(socket, opts) { function terminate(socket, opts) {
console.log('[tls-terminate] ' + opts.localAddress || socket.localAddress + ':' + opts.localPort || socket.localPort + ' servername', opts.servername, socket.remoteAddress); console.log(
'[tls-terminate]'
, opts.localAddress || socket.localAddress +':'+ opts.localPort || socket.localPort
, 'servername=' + opts.servername
, opts.remoteAddress || socket.remoteAddress
);
if (opts.hyperPeek) { if (opts.hyperPeek) {
// This connection was peeked at using a method that doesn't interferre with the TLS // This connection was peeked at using a method that doesn't interferre with the TLS
// server's ability to handle it properly. Currently the only way this happens is // server's ability to handle it properly. Currently the only way this happens is
// with tunnel connections where we have the first chunk of data before creating the // with tunnel connections where we have the first chunk of data before creating the
// new connection (thus removing need to get data off the new connection). // new connection (thus removing need to get data off the new connection).
terminator.emit('connection', socket); terminateServer.emit('connection', socket);
return;
} }
else {
// The hyperPeek flag wasn't set, so we had to read data off of this connection, which // The hyperPeek flag wasn't set, so we had to read data off of this connection, which
// means we can no longer use it directly in the TLS server. // means we can no longer use it directly in the TLS server.
// See https://github.com/nodejs/node/issues/8752 (node's internal networking layer == 💩 sometimes) // See https://github.com/nodejs/node/issues/8752 (node's internal networking layer == 💩 sometimes)
var myDuplex = require('tunnel-packer').Stream.create(socket); terminateServer.emit('connection', wrapSocket(socket, opts));
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();
});
terminator.emit('connection', myDuplex);
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(opts.firstChunk);
});
} }
function handleConn(socket, opts) { function handleConn(socket, opts) {

32
lib/proxy-err-resp.js Normal file
View File

@ -0,0 +1,32 @@
'use strict';
function getRespBody(err, debug) {
if (debug) {
return err.toString();
}
if (err.code === 'ECONNREFUSED') {
return 'The connection was refused. Most likely the service being connected to '
+ 'has stopped running or the configuration is wrong.';
}
return 'Bad Gateway: ' + err.code;
}
function sendBadGateway(conn, err, debug) {
var body = getRespBody(err, debug);
conn.write([
'HTTP/1.1 502 Bad Gateway'
, 'Date: ' + (new Date()).toUTCString()
, 'Connection: close'
, 'Content-Type: text/html'
, 'Content-Length: ' + body.length
, ''
, body
].join('\r\n'));
conn.end();
}
module.exports.getRespBody = getRespBody;
module.exports.sendBadGateway = sendBadGateway;