-
Notifications
You must be signed in to change notification settings - Fork 33
Automagic socket sharing for net/tls connections via proxy server #702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d0d2e19
a959ccf
461fce7
a84d76d
df6b08d
1fd0f23
88549e2
ea020f1
3517f12
5eaabb1
37b4ef6
68d177b
cb8ebfc
c79791e
73c4203
df06435
81e2541
1de84f0
45ad7d5
e1de8fa
23c6177
196ec35
0f6fc6c
127c9db
d3466c8
e0c804b
dd56bcf
1698c33
769ff68
c3dc3d5
f5b4340
7cec5f7
f97fd03
bcb8a19
7c3421c
2ecc1fa
24ec2b8
aa1fe42
2fdfd78
fdc6ae3
f01660a
b615030
2c8f8f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| var util = require('util'), | ||
| events = require('events'), | ||
| net = require('net'), | ||
| tls = require('tls'), | ||
| streamplex = require('_streamplex'); | ||
|
|
||
| // NOTE: this list may not be exhaustive, see also https://tools.ietf.org/html/rfc5735#section-4 | ||
| var _PROXY_LOCAL = "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 127.0.0.0/8 localhost"; | ||
|
|
||
| var _PROXY_DBG = ('_PROXY_DBG' in process.env) || false, | ||
| PROXY_HOST = process.env.PROXY_HOST || "proxy.tessel.io", | ||
| PROXY_PORT = +process.env.PROXY_PORT || 443, | ||
| PROXY_TRUSTED = +process.env.PROXY_TRUSTED || 0, | ||
| PROXY_TOKEN = process.env.PROXY_TOKEN || process.env.TM_API_KEY, | ||
| PROXY_LOCAL = process.env.PROXY_LOCAL || _PROXY_LOCAL, | ||
| PROXY_IDLE = +process.env.PROXY_IDLE || 90e3, | ||
| PROXY_CERT = process.env.PROXY_CERT || null; | ||
|
|
||
| /** | ||
| * Tunnel helpers | ||
| */ | ||
|
|
||
| function createTunnel(cb) { | ||
| if (_PROXY_DBG) console.log("TUNNEL -> START", new Date()); | ||
| tls.connect({host:PROXY_HOST, port:PROXY_PORT, proxy:false, ca:(PROXY_CERT && [PROXY_CERT])}, function () { | ||
| var proxySocket = this, | ||
| tunnel = streamplex(streamplex.B_SIDE); | ||
| tunnel.pipe(proxySocket).pipe(tunnel); | ||
| proxySocket.on('error', shutdownTunnel); | ||
| proxySocket.on('close', shutdownTunnel); | ||
| proxySocket.on('error', cb); | ||
|
|
||
| var idleTimeout; | ||
| tunnel.on('inactive', function () { | ||
| if (_PROXY_DBG) console.log("TUNNEL -> inactive", new Date()); | ||
| idleTimeout = setTimeout(shutdownTunnel, PROXY_IDLE); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should idleTimeout be cleared if it is already set? (Failsafe)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The timeout is cleared by the 'active' event which would have to happen before another 'inactive' one. |
||
| }); | ||
| tunnel.on('active', function () { | ||
| if (_PROXY_DBG) console.log("TUNNEL -> active", new Date()); | ||
| clearTimeout(idleTimeout); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be failsafey, maybe this?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if clearTimeout follows spec, it should be just fine to call it with null (or any other invalid!) values. |
||
| }); | ||
|
|
||
| tunnel.sendMessage({token:PROXY_TOKEN}); | ||
| tunnel.once('message', function (d) { | ||
| if (_PROXY_DBG) console.log("TUNNEL: auth response?", d); | ||
| proxySocket.removeListener('error', cb); | ||
| if (!d.authed) cb(new Error("Authorization failed.")); | ||
| else cb(null, tunnel); | ||
| }); | ||
| function shutdownTunnel(e) { | ||
| if (_PROXY_DBG) console.log("TUNNEL -> STOP", new Date()); | ||
| tunnel.destroy(e); | ||
| if (this !== proxySocket) proxySocket.end(); | ||
| proxySocket.removeListener('close', shutdownTunnel); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this also be removed as the
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess including clearTimeout
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing the 'close' listener is specifically to prevent |
||
| } | ||
| }).on('error', cb); | ||
| } | ||
|
|
||
| var tunnelKeeper = new events.EventEmitter(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am the Tunnel Master I am the Gate Keeper
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in commit ecdoaf6bf5a2b1 |
||
|
|
||
| tunnelKeeper.getTunnel = function (cb) { // CAUTION: syncronous callback! | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does this have to be synchronous? (Rather than,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is internal code, I didn't see a reason to artificially delay |
||
| if (this._tunnel) return cb(null, this._tunnel); | ||
|
|
||
| var self = this; | ||
| if (!this._pending) createTunnel(function (e, tunnel) { | ||
| delete self._pending; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (e) return self.emit('tunnel', e); | ||
|
|
||
| self._tunnel = tunnel; | ||
| tunnel.on('close', function () { | ||
| self._tunnel = null; | ||
| }); | ||
| var streamProto = Object.create(ProxiedSocket.prototype); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just curious, why object.create instead of new?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If it weren't for needing a link between socket instances and their associated tunnel (next two lines) the logic that uses this would just |
||
| streamProto._tunnel = tunnel; | ||
| tunnel._streamProto = streamProto; | ||
| self.emit('tunnel', null, tunnel); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd call you out on doing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is an internal event, whose parameters are always |
||
| }); | ||
| this._pending = true; | ||
| this.once('tunnel', cb); | ||
| }; | ||
|
|
||
| var local_matchers = PROXY_LOCAL.split(' ').map(function (str) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See PROXY_LOCAL splitting comment earlier
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incidentally, can we get a test case in for the local_matchers generator? Since it's a lot of bit fiddling, it'd be good to ensure it never regresses subtley
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah…agree this is a pretty reasonable thing to have test coverage for. I suppose I'll need to refactor this a bit and expose via an [underscored] property on the [underscored] module for it. |
||
| var parts = str.split('/'); | ||
| if (parts.length > 1) { | ||
| // IPv4 + mask | ||
| var bits = +parts[1], | ||
| mask = 0xFFFFFFFF << (32-bits) >>> 0, | ||
| base = net._ipStrToInt(parts[0]) & mask; // NOTE: left signed to match test below | ||
| return function (addr, host) { | ||
| return ((addr & mask) === base); | ||
| }; | ||
| } else if (str[0] === '.') { | ||
| // base including subdomains | ||
| str = str.slice(1); | ||
| return function (addr, host) { | ||
| var idx = host.lastIndexOf(str); | ||
| return (~idx && idx + str.length === host.length); | ||
| }; | ||
| } else return function (addr, host) { | ||
| // exact domain/address | ||
| return (host === str); | ||
| } | ||
| }); | ||
|
|
||
| function protoForConnection(host, port, opts, cb) { // CAUTION: syncronous callback! | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same: can just be made to always be async?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would be if the optimization above is removed. |
||
| var addr = (net.isIPv4(host)) ? net._ipStrToInt(host) : null, | ||
| force_local = !PROXY_TOKEN || (opts._secure && !PROXY_TRUSTED) || (opts.proxy === false), | ||
| local = force_local || local_matchers.some(function (matcher) { return matcher(addr, host); }); | ||
| if (_PROXY_DBG) { | ||
| if (force_local) console.log( | ||
| "Forced to use local socket to \"%s\". [token: %s, secure/trusted: %s/%s, opts override: %s]", | ||
| host, Boolean(PROXY_TOKEN), Boolean(opts._secure), Boolean(PROXY_TRUSTED), (opts.proxy === false) | ||
| ); | ||
| else console.log("Proxied socket to \"%s\"? %s", host, !local); | ||
| } | ||
| if (local) cb(null, net._CC3KSocket.prototype); | ||
| else tunnelKeeper.getTunnel(function (e, tunnel) { | ||
| if (e) return cb(e); | ||
| cb(null, tunnel._streamProto); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * ProxiedSocket | ||
| */ | ||
|
|
||
| function ProxiedSocket(opts) { | ||
| if (!(this instanceof ProxiedSocket)) return new ProxiedSocket(opts); | ||
| net.Socket.call(this, opts); | ||
| this._tunnel = this._opts.tunnel; | ||
| this._setup(this._opts); | ||
| } | ||
| util.inherits(ProxiedSocket, net.Socket); | ||
|
|
||
| ProxiedSocket.prototype._setup = function () { | ||
| var type = (this._secure) ? 'tls' : 'net'; | ||
| this._transport = this._tunnel.createStream(type); | ||
|
|
||
| var self = this; | ||
| // TODO: it'd be great if we is-a substream instead of has-a… | ||
| this._transport.on('data', function (d) { | ||
| var more = self.push(d); | ||
| if (!more) self._transport.pause(); | ||
| }); | ||
| this._transport.on('end', function () { | ||
| self.push(null); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is self._transport need any cleanup past
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not quite sure this is what you're asking, but ProxiedSocket may never emit a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 0674dbe |
||
| }); | ||
|
|
||
| function reEmit(evt) { | ||
| self._transport.on(evt, function test() { | ||
| var args = Array.prototype.concat.apply([evt], arguments); | ||
| self.emit.apply(self, args); | ||
| }); | ||
| } | ||
| ['connect', 'secureConnect', 'error', 'timeout', 'close'].forEach(reEmit); | ||
| }; | ||
|
|
||
| ProxiedSocket.prototype._read = function () { | ||
| this._transport.resume(); | ||
| }; | ||
| ProxiedSocket.prototype._write = function (buf, enc, cb) { | ||
| this._transport.write(buf, enc, cb); | ||
| }; | ||
|
|
||
| ProxiedSocket.prototype._connect = function (port, host) { | ||
| this.remotePort = port; | ||
| this.remoteAddress = host; | ||
| this._transport.remoteEmit('_pls_connect', port, host); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OP PLS |
||
| }; | ||
|
|
||
| ProxiedSocket.prototype.setTimeout = function (msecs, cb) { | ||
| this._transport.remoteEmit('_pls_timeout', msecs); | ||
| if (cb) { | ||
| if (msecs) this.once('timeout', cb); | ||
| else this.removeListener('timeout', cb); | ||
| } | ||
| }; | ||
|
|
||
| ProxiedSocket.prototype.destroy = function () { | ||
| this._transport.destroy(); | ||
| this.end(); | ||
| }; | ||
|
|
||
| exports._protoForConnection = protoForConnection; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Want to move splitting logic to here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not particularly, the env variable as defaulted here gets processed all in once place later I don't see a compelling reason to split the
splitpart of that logic away?