Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion bin/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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, () => {
Expand Down
4 changes: 4 additions & 0 deletions lib/ClientManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand All @@ -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 = {
Expand All @@ -39,6 +41,7 @@ class ClientManager {

const maxSockets = this.opt.max_tcp_sockets;
const agent = new TunnelAgent({
portManager: this.portManager,
clientId: id,
maxSockets: 10,
});
Expand Down Expand Up @@ -79,6 +82,7 @@ class ClientManager {
if (!client) {
return;
}
this.portManager.release(client.agent.port);
--this.stats.tunnels;
delete this.clients[id];
client.close();
Expand Down
61 changes: 61 additions & 0 deletions lib/PortManager.js
Original file line number Diff line number Diff line change
@@ -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;

52 changes: 52 additions & 0 deletions lib/PortManager.test.js
Original file line number Diff line number Diff line change
@@ -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'));
});
});

16 changes: 12 additions & 4 deletions lib/TunnelAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
});

Expand Down