Add a port pool manager to restrict client ports #134
@@ -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, () => {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user