forward is based on incoming port, while proxy is based on domains and we don't have any domain names for raw UDP or TCP
242 lines
7.8 KiB
JavaScript
242 lines
7.8 KiB
JavaScript
'use strict';
|
|
|
|
module.exports.create = function (deps, config) {
|
|
console.log('config', config);
|
|
|
|
//var PromiseA = global.Promise;
|
|
var PromiseA = require('bluebird');
|
|
var listeners = require('./servers').listeners;
|
|
var modules;
|
|
|
|
function loadModules() {
|
|
modules = {};
|
|
|
|
modules.tls = require('./modules/tls').create(deps, config, netHandler);
|
|
modules.http = require('./modules/http.js').create(deps, config, modules.tls.middleware);
|
|
}
|
|
|
|
// opts = { servername, encrypted, peek, data, remoteAddress, remotePort }
|
|
function peek(conn, firstChunk, opts) {
|
|
if (!modules) {
|
|
loadModules();
|
|
}
|
|
|
|
opts.firstChunk = firstChunk;
|
|
conn.__opts = opts;
|
|
// TODO port/service-based routing can do here
|
|
|
|
// TLS byte 1 is handshake and byte 6 is client hello
|
|
if (0x16 === firstChunk[0]/* && 0x01 === firstChunk[5]*/) {
|
|
modules.tls.emit('connection', conn);
|
|
return;
|
|
}
|
|
|
|
// This doesn't work with TLS, but now that we know this isn't a TLS connection we can
|
|
// unshift the first chunk back onto the connection for future use. The unshift should
|
|
// happen after any listeners are attached to it but before any new data comes in.
|
|
if (!opts.hyperPeek) {
|
|
process.nextTick(function () {
|
|
conn.unshift(firstChunk);
|
|
});
|
|
}
|
|
|
|
// Connection is not TLS, check for HTTP next.
|
|
if (firstChunk[0] > 32 && firstChunk[0] < 127) {
|
|
var firstStr = firstChunk.toString();
|
|
if (/HTTP\//i.test(firstStr)) {
|
|
modules.http.emit('connection', conn);
|
|
return;
|
|
}
|
|
}
|
|
|
|
console.warn('failed to identify protocol from first chunk', firstChunk);
|
|
conn.destroy();
|
|
}
|
|
function netHandler(conn, opts) {
|
|
function getProp(name) {
|
|
return opts[name] || opts['_'+name] || conn[name] || conn['_'+name];
|
|
}
|
|
opts = opts || {};
|
|
var logName = getProp('remoteAddress') + ':' + getProp('remotePort') + ' -> ' +
|
|
getProp('localAddress') + ':' + getProp('localPort');
|
|
console.log('[netHandler]', logName, 'encrypted: '+opts.encrypted);
|
|
|
|
var start = Date.now();
|
|
conn.on('timeout', function () {
|
|
console.log('[netHandler]', logName, 'connection timed out', (Date.now()-start)/1000);
|
|
});
|
|
conn.on('end', function () {
|
|
console.log('[netHandler]', logName, 'connection ended', (Date.now()-start)/1000);
|
|
});
|
|
conn.on('close', function () {
|
|
console.log('[netHandler]', logName, 'connection closed', (Date.now()-start)/1000);
|
|
});
|
|
|
|
// XXX PEEK COMMENT XXX
|
|
// TODO we can have our cake and eat it too
|
|
// we can skip the need to wrap the TLS connection twice
|
|
// because we've already peeked at the data,
|
|
// but this needs to be handled better before we enable that
|
|
// (because it creates new edge cases)
|
|
if (opts.hyperPeek) {
|
|
console.log('hyperpeek');
|
|
peek(conn, opts.firstChunk, opts);
|
|
return;
|
|
}
|
|
|
|
function onError(err) {
|
|
console.error('[error] socket errored peeking -', err);
|
|
conn.destroy();
|
|
}
|
|
conn.once('error', onError);
|
|
conn.once('data', function (chunk) {
|
|
conn.removeListener('error', onError);
|
|
peek(conn, chunk, opts);
|
|
});
|
|
}
|
|
|
|
function dnsListener(port, msg) {
|
|
if (!Array.isArray(config.udp.modules)) {
|
|
return;
|
|
}
|
|
var socket = require('dgram').createSocket('udp4');
|
|
config.udp.modules.forEach(function (mod) {
|
|
if (mod.type !== 'forward') {
|
|
console.warn('found bad DNS module', mod);
|
|
return;
|
|
}
|
|
if (mod.ports.indexOf(port) < 0) {
|
|
return;
|
|
}
|
|
|
|
var dest = require('./domain-utils').separatePort(mod.address || '');
|
|
dest.port = dest.port || mod.port;
|
|
dest.host = dest.host || mod.host || 'localhost';
|
|
socket.send(msg, dest.port, dest.host);
|
|
});
|
|
}
|
|
|
|
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 = {};
|
|
['remote', 'local'].forEach(function (end) {
|
|
['Family', 'Address', 'Port'].forEach(function (name) {
|
|
newConnOpts['_'+end+name] = conn[end+name];
|
|
});
|
|
});
|
|
|
|
deps.proxy(conn, Object.assign(newConnOpts, dest));
|
|
};
|
|
}
|
|
|
|
deps.tunnel = deps.tunnel || {};
|
|
deps.tunnel.net = {
|
|
createConnection: function (opts, cb) {
|
|
console.log('[gl.tunnel] creating connection');
|
|
|
|
// here "reader" means the socket that looks like the connection being accepted
|
|
// here "writer" means the remote-looking part of the socket that driving the connection
|
|
var writer;
|
|
var wrapOpts = {};
|
|
|
|
function usePair(err, reader) {
|
|
if (err) {
|
|
process.nextTick(function () {
|
|
writer.emit('error', err);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// this has the normal net/tcp stuff plus our custom stuff
|
|
// opts = { address, port,
|
|
// hostname, servername, tls, encrypted, data, localAddress, localPort, remoteAddress, remotePort, remoteFamily }
|
|
Object.keys(opts).forEach(function (key) {
|
|
wrapOpts[key] = opts[key];
|
|
try {
|
|
reader[key] = opts[key];
|
|
} catch(e) {
|
|
// can't set real socket getters, like remoteAddr
|
|
}
|
|
});
|
|
|
|
// A few more extra specialty options
|
|
wrapOpts.localAddress = wrapOpts.localAddress || '127.0.0.2'; // TODO use the tunnel's external address
|
|
wrapOpts.localPort = wrapOpts.localPort || 'tunnel-0';
|
|
try {
|
|
reader._remoteAddress = wrapOpts.remoteAddress;
|
|
reader._remotePort = wrapOpts.remotePort;
|
|
reader._remoteFamily = wrapOpts.remoteFamily;
|
|
reader._localAddress = wrapOpts.localAddress;
|
|
reader._localPort = wrapOpts.localPort;
|
|
reader._localFamily = wrapOpts.localFamily;
|
|
} catch(e) {
|
|
}
|
|
|
|
netHandler(reader, wrapOpts);
|
|
|
|
process.nextTick(function () {
|
|
// this cb will cause the stream to emit its (actually) first data event
|
|
// (even though it already gave a peek into that first data chunk)
|
|
console.log('[tunnel] callback, data should begin to flow');
|
|
cb();
|
|
});
|
|
}
|
|
|
|
wrapOpts.firstChunk = opts.data;
|
|
wrapOpts.hyperPeek = !!opts.data;
|
|
|
|
// We used to use `stream-pair` for non-tls connections, but there are places
|
|
// that require properties/functions to be present on the socket that aren't
|
|
// present on a JSStream so it caused problems.
|
|
writer = require('socket-pair').create(usePair);
|
|
return writer;
|
|
}
|
|
};
|
|
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;
|
|
});
|
|
|
|
(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 {
|
|
console.warn('unknown TCP module specified', mod);
|
|
}
|
|
});
|
|
|
|
var portList = Object.keys(tcpPortMap).map(Number).sort();
|
|
portList.forEach(function (port) {
|
|
listenPromises.push(listeners.tcp.add(port, netHandler));
|
|
});
|
|
|
|
if (config.udp.bind) {
|
|
config.udp.bind.forEach(function (port) {
|
|
listenPromises.push(listeners.udp.add(port, dnsListener.bind(port)));
|
|
});
|
|
}
|
|
|
|
if (!config.mdns.disabled) {
|
|
require('./mdns').start(deps, config, portList[0]);
|
|
}
|
|
|
|
return PromiseA.all(listenPromises);
|
|
};
|