diff --git a/bin/server b/bin/server index a6bba9d8..10fd77d0 100755 --- a/bin/server +++ b/bin/server @@ -31,6 +31,10 @@ const argv = optimist default: 10, describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' }) + .options('range', { + default: null, + describe: 'will bind incoming connections only on ports in range xxx:xxxx' + }) .argv; if (argv.help) { @@ -42,6 +46,7 @@ const server = CreateServer({ max_tcp_sockets: argv['max-sockets'], secure: argv.secure, domain: argv.domain, + range: argv.range, }); server.listen(argv.port, argv.address, () => { diff --git a/lib/ClientManager.js b/lib/ClientManager.js index e1a78386..6d97265c 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -3,6 +3,7 @@ import Debug from 'debug'; import Client from './Client'; import TunnelAgent from './TunnelAgent'; +import PortManager from "./PortManager"; // Manage sets of clients // @@ -13,6 +14,7 @@ class ClientManager { // id -> client instance this.clients = new Map(); + this.portManager = new PortManager({range: this.opt.range||null}) // statistics this.stats = { @@ -39,6 +41,7 @@ class ClientManager { const maxSockets = this.opt.max_tcp_sockets; const agent = new TunnelAgent({ + portManager: this.portManager, clientId: id, maxSockets: 10, }); @@ -79,6 +82,7 @@ class ClientManager { if (!client) { return; } + this.portManager.release(client.agent.port); --this.stats.tunnels; delete this.clients[id]; client.close(); diff --git a/lib/PortManager.js b/lib/PortManager.js new file mode 100644 index 00000000..cffc4ac8 --- /dev/null +++ b/lib/PortManager.js @@ -0,0 +1,60 @@ +import Debug from 'debug'; + +class PortManager { + constructor(opt) { + this.debug = Debug('lt:PortManager'); + this.range = opt.range || null; + this.first = null; + this.last = null; + this.pool = {}; + this.initializePool(); + } + + initializePool() { + if (this.range === null) { + return; + } + + if (!/^[0-9]+:[0-9]+$/.test(this.range)) { + throw new Error('Bad range expression: ' + this.range); + } + + [this.first, this.last] = this.range.split(':').map((port) => parseInt(port)); + + if (this.first > this.last) { + throw new Error('Bad range expression min > max: ' + this.range); + } + + for (let port = this.first; port <= this.last; port++) { + this.pool['_' + port] = null; + } + this.debug = Debug('lt:PortManager'); + this.debug('Pool initialized ' + JSON.stringify(this.pool)); + } + + release(port) { + if (this.range === null) { + return; + } + this.debug('Release port ' + port); + this.pool['_' + port] = null; + } + + getNextAvailable(clientId) { + if (this.range === null) { + return null; + } + + for (let port = this.first; port <= this.last; port++) { + if (this.pool['_' + port] === null) { + this.pool['_' + port] = clientId; + this.debug('Port found ' + port); + return port; + } + } + this.debug('No more ports available '); + throw new Error('No more ports available in range ' + this.range); + } +} + +export default PortManager; diff --git a/lib/PortManager.test.js b/lib/PortManager.test.js new file mode 100644 index 00000000..eb403e13 --- /dev/null +++ b/lib/PortManager.test.js @@ -0,0 +1,51 @@ +import assert from 'assert'; + +import PortManager from './PortManager'; + +describe('PortManager', () => { + it('should construct with no range', () => { + const portManager = new PortManager({}); + assert.equal(portManager.range, null); + assert.equal(portManager.first, null); + assert.equal(portManager.last, null); + }); + + it('should construct with range', () => { + const portManager = new PortManager({range: '10:20'}); + assert.equal(portManager.range, '10:20'); + assert.equal(portManager.first, 10); + assert.equal(portManager.last, 20); + }); + + it('should not construct with bad range expression', () => { + assert.throws(()=>{ + new PortManager({range: 'a1020'}); + }, /Bad range expression: a1020/) + }); + + it('should not construct with bad range max>min', () => { + assert.throws(()=>{ + new PortManager({range: '20:10'}); + }, /Bad range expression min > max: 20:10/) + }); + + it('should work has expected', async () => { + const portManager = new PortManager({range: '10:12'}); + assert.equal(10,portManager.getNextAvailable('a')); + assert.equal(11,portManager.getNextAvailable('b')); + assert.equal(12,portManager.getNextAvailable('c')); + + assert.throws(()=>{ + portManager.getNextAvailable(); + }, /No more ports available in range 10:12/) + + portManager.release(11); + assert.equal(11,portManager.getNextAvailable('bb')); + + portManager.release(10); + portManager.release(12); + + assert.equal(10,portManager.getNextAvailable('cc')); + assert.equal(12,portManager.getNextAvailable('dd')); + }); +}); diff --git a/lib/TunnelAgent.js b/lib/TunnelAgent.js index efc2231b..a953d726 100644 --- a/lib/TunnelAgent.js +++ b/lib/TunnelAgent.js @@ -20,6 +20,9 @@ class TunnelAgent extends Agent { // sockets we can hand out via createConnection this.availableSockets = []; + this.port = null; + this.clientId = options.clientId + this.portManager = options.portManager || null; // when a createConnection cannot return a socket, it goes into a queue // once a socket is available it is handed out to the next callback @@ -63,13 +66,14 @@ class TunnelAgent extends Agent { }); return new Promise((resolve) => { - server.listen(() => { - const port = server.address().port; - this.debug('tcp server listening on port: %d', port); + const port = this.portManager ? this.portManager.getNextAvailable(this.options.clientId) : null; + server.listen(port,() => { + this.port = server.address().port + this.debug('tcp server listening on port: %d (%s)', this.port, this.clientId); resolve({ // port for lt client tcp connections - port: port, + port: this.port, }); }); }); @@ -115,6 +119,9 @@ class TunnelAgent extends Agent { socket.once('error', (err) => { // we do not log these errors, sessions can drop from clients for many reasons // these are not actionable errors for our server + if(this.portManager){ + this.portManager.release(this.port); + } socket.destroy(); });