diff --git a/bin/server.js b/bin/server.js index f67f6b60..5b01d0e1 100755 --- a/bin/server.js +++ b/bin/server.js @@ -36,6 +36,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) { @@ -47,7 +51,8 @@ const server = CreateServer({ max_tcp_sockets: argv['max-sockets'], secure: argv.secure, domain: argv.domain, - landing: argv.landing + landing: argv.landing, + range: argv.range }); server.listen(argv.port, argv.address, () => { diff --git a/lib/ClientManager.js b/lib/ClientManager.js index 4758dabc..57d94db1 100644 --- a/lib/ClientManager.js +++ b/lib/ClientManager.js @@ -3,6 +3,7 @@ import Debug from 'debug'; import Client from './Client.js'; import TunnelAgent from './TunnelAgent.js'; +import PortManager from './PortManager.js'; // 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..a528f977 --- /dev/null +++ b/lib/PortManager.js @@ -0,0 +1,61 @@ +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..e71f3092 --- /dev/null +++ b/lib/PortManager.test.js @@ -0,0 +1,52 @@ +import assert from 'assert'; + +import PortManager from './PortManager.js'; + +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 e76b3112..4c5b0583 100644 --- a/lib/TunnelAgent.js +++ b/lib/TunnelAgent.js @@ -41,6 +41,10 @@ class TunnelAgent extends Agent { this.waitingCreateConn = []; this.debug = Debug(`lt:TunnelAgent[${options.clientId}]`); + + this.port = null; + this.clientId = options.clientId; + this.portManager = options.portManager || null; // track maximum allowed sockets this.connectedSockets = 0; @@ -81,13 +85,14 @@ class TunnelAgent extends Agent { }); return new Promise((resolve) => { - this.server.listen(async () => { - const port = this.server.address().port; - this.debug('tcp server listening on port: %d', port); + const port = this.portManager ? this.portManager.getNextAvailable(this.clientId) : null; + this.server.listen(port, async () => { + this.port = this.server.address().port; + this.debug('tcp server listening on port: %d (%s)', this.port, this.clientId); const info = { // port for lt client tcp connections - port: port, + port: this.port, }; if (!PUBLIC_IP) PUBLIC_IP = await getPublicIPv4(); @@ -146,6 +151,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(); });