Skip to content
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

Add a port pool manager to restrict client ports #134

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions bin/server
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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, () => {
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';
import TunnelAgent from './TunnelAgent';
import PortManager from "./PortManager";

// 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
60 changes: 60 additions & 0 deletions lib/PortManager.js
Original file line number Diff line number Diff line change
@@ -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;
51 changes: 51 additions & 0 deletions lib/PortManager.test.js
Original file line number Diff line number Diff line change
@@ -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'));
});
});
15 changes: 11 additions & 4 deletions lib/TunnelAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
});
});
});
Expand Down Expand Up @@ -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();
});

Expand Down