Add a port pool manager to restrict client ports #134

Open
micoli wants to merge 2 commits from micoli/add_a_port_manager into master
5 changed files with 131 additions and 4 deletions
+5
View File
@@ -31,6 +31,10 @@ const argv = optimist
default: 10, default: 10,
describe: 'maximum number of tcp sockets each client is allowed to establish at one time (the tunnels)' 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; .argv;
if (argv.help) { if (argv.help) {
@@ -42,6 +46,7 @@ const server = CreateServer({
max_tcp_sockets: argv['max-sockets'], max_tcp_sockets: argv['max-sockets'],
secure: argv.secure, secure: argv.secure,
domain: argv.domain, domain: argv.domain,
range: argv.range,
}); });
server.listen(argv.port, argv.address, () => { server.listen(argv.port, argv.address, () => {
+4
View File
@@ -3,6 +3,7 @@ import Debug from 'debug';
import Client from './Client'; import Client from './Client';
import TunnelAgent from './TunnelAgent'; import TunnelAgent from './TunnelAgent';
import PortManager from "./PortManager";
// Manage sets of clients // Manage sets of clients
// //
@@ -13,6 +14,7 @@ class ClientManager {
// id -> client instance // id -> client instance
this.clients = new Map(); this.clients = new Map();
this.portManager = new PortManager({range: this.opt.range||null})
// statistics // statistics
this.stats = { this.stats = {
@@ -39,6 +41,7 @@ class ClientManager {
const maxSockets = this.opt.max_tcp_sockets; const maxSockets = this.opt.max_tcp_sockets;
const agent = new TunnelAgent({ const agent = new TunnelAgent({
portManager: this.portManager,
clientId: id, clientId: id,
maxSockets: 10, maxSockets: 10,
}); });
@@ -79,6 +82,7 @@ class ClientManager {
if (!client) { if (!client) {
return; return;
} }
this.portManager.release(client.agent.port);
--this.stats.tunnels; --this.stats.tunnels;
delete this.clients[id]; delete this.clients[id];
client.close(); client.close();
+60
View File
@@ -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
View File
@@ -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'));
});
});
+11 -4
View File
@@ -20,6 +20,9 @@ class TunnelAgent extends Agent {
// sockets we can hand out via createConnection // sockets we can hand out via createConnection
this.availableSockets = []; 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 // 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 // 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) => { return new Promise((resolve) => {
server.listen(() => { const port = this.portManager ? this.portManager.getNextAvailable(this.options.clientId) : null;
const port = server.address().port; server.listen(port,() => {
this.debug('tcp server listening on port: %d', port); this.port = server.address().port
this.debug('tcp server listening on port: %d (%s)', this.port, this.clientId);
resolve({ resolve({
// port for lt client tcp connections // port for lt client tcp connections
port: port, port: this.port,
}); });
}); });
}); });
@@ -115,6 +119,9 @@ class TunnelAgent extends Agent {
socket.once('error', (err) => { socket.once('error', (err) => {
// we do not log these errors, sessions can drop from clients for many reasons // we do not log these errors, sessions can drop from clients for many reasons
// these are not actionable errors for our server // these are not actionable errors for our server
if(this.portManager){
this.portManager.release(this.port);
}
socket.destroy(); socket.destroy();
}); });