diff --git a/README.md b/README.md index 4817fcd..6990b71 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Features * Optimized generation transaction building * Connecting to multiple daemons for redundancy * Process share submissions +* Session managing for purging DDoS/flood initiated fake workers +* Auto ban IPs that are flooding with invalid shares * __POW__ (proof-of-work) & __POS__ (proof-of-stake) support * Transaction messages support * Vardiff (variable difficulty / share limiter) @@ -49,7 +51,6 @@ Features #### To do * Statistics module -* Auto-banning flooders Requirements @@ -87,8 +88,20 @@ var pool = Stratum.createPool({ //instanceId: 37, //Recommend not using this because a crypto-random one will be generated + /* Some attackers will create thousands of workers that use up all available socket connections, + usually the workers are zombies and don't submit shares after connecting. This features + detects those and disconnects them */ "connectionTimeout": 120, //Remove workers that haven't been in contact for this many seconds + /* If a worker is submitting a good deal of invalid shares we can temporarily ban them to + reduce system/network load. Also useful to fight against flooding attacks. */ + "banning": { + "enabled": true, + "time": 600, //How many seconds to ban worker for + "invalidPercent": 50, //What percent of invalid shares triggers ban + "checkThreshold": 500 //Check invalid percent when this many shares have been submitted + }, + /* Each pool can have as many ports for your miners to connect to as you wish. Each port can be configured to use its own pool difficulty and variable difficulty settings. varDiff is optional and will only be used for the ports you configure it for. */ diff --git a/lib/pool.js b/lib/pool.js index 9d285ff..dbedcb8 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -101,7 +101,7 @@ var pool = module.exports = function pool(options, authorizeFn){ } else{ rpcCommand = 'getblocktemplate'; - rcpArgs = [{'mode': 'submit', 'data': blockHex}]; + rpcArgs = [{'mode': 'submit', 'data': blockHex}]; } @@ -328,7 +328,7 @@ var pool = module.exports = function pool(options, authorizeFn){ function StartStratumServer(){ - _this.stratumServer = new stratum.Server(options.ports, options.connectionTimeout, authorizeFn); + _this.stratumServer = new stratum.Server(options.ports, options.connectionTimeout, options.banning, authorizeFn); _this.stratumServer.on('started', function(){ emitLog('system','Stratum server started on port(s): ' + Object.keys(options.ports).join(', ')); _this.emit('started'); @@ -365,15 +365,18 @@ var pool = module.exports = function pool(options, authorizeFn){ resultCallback(result.error, result.result ? true : null); }).on('malformedMessage', function (message) { - emitWarningLog('client', client.workerName+" has sent us a malformed message: "+message); + emitWarningLog('client', client.workerName + " has sent us a malformed message: " + message); }).on('socketError', function() { - emitWarningLog('client', client.workerName+" has somehow had a socket error"); + emitWarningLog('client', client.workerName + " has somehow had a socket error"); }).on('socketDisconnect', function() { - emitLog('client', "Client '"+client.workerName+"' disconnected!"); + emitLog('client', "Client '" + client.workerName + "' disconnected!"); }).on('unknownStratumMethod', function(fullMessage) { - emitLog('client', "Client '"+client.workerName+"' has sent us an unknown stratum method: "+fullMessage.method); + emitLog('client', "Client '" + client.workerName + "' has sent us an unknown stratum method: " + fullMessage.method); }).on('socketFlooded', function(){ emitWarningLog('client', 'Detected socket flooding and purged buffer'); + }).on('ban', function(ipAddress){ + _this.emit('banIP', ipAddress); + emitWarningLog('client', 'banned IP ' + ipAddress); }); }); } @@ -441,7 +444,7 @@ var pool = module.exports = function pool(options, authorizeFn){ emitErrorLog('system', 'Block notify error getting block template for ' + options.coin.name); }) } - } + }; this.relinquishMiners = function(filterFn, resultCback) { var origStratumClients = this.stratumServer.getStratumClients(); diff --git a/lib/stratum.js b/lib/stratum.js index 7079684..d082446 100644 --- a/lib/stratum.js +++ b/lib/stratum.js @@ -28,10 +28,28 @@ var StratumClient = function(options){ //private members this.socket = options.socket; + var banning = options.banning; + var _this = this; this.lastActivity = Date.now(); + this.shares = {valid: 0, invalid: 0}; + + var considerBan = !banning.enabled ? function(){} : function(shareValid){ + if (shareValid === true) _this.shares.valid++; + else _this.shares.invalid++; + var totalShares = _this.shares.valid + _this.shares.invalid; + if (totalShares >= banning.checkThreshold){ + var percentBad = (_this.shares.invalid / totalShares) * 100; + if (percentBad >= banning.invalidPercent){ + _this.emit('ban', _this.socket.remoteAddress); + _this.socket.end(); + } + else //reset shares + this.shares = {valid: 0, invalid: 0}; + } + }; (function init(){ setupSocket(); @@ -120,6 +138,7 @@ var StratumClient = function(options){ result: null, error : [24, "unauthorized worker", null] }); + considerBan(false); return; } if (!_this.extraNonce1){ @@ -128,6 +147,7 @@ var StratumClient = function(options){ result: null, error : [25, "not subscribed", null] }); + considerBan(false); return; } _this.emit('submit', @@ -139,6 +159,7 @@ var StratumClient = function(options){ nonce : message.params[4] }, function(error, result){ + considerBan(result); sendJson({ id : message.id, result : result, @@ -226,6 +247,12 @@ var StratumClient = function(options){ }; this.sendMiningJob = function(jobParams){ + + if (Date.now() - _this.lastActivity > options.socketTimeout){ + _this.socket.end(); + return; + } + if (pendingDifficulty !== null){ var result = _this.sendDifficulty(pendingDifficulty); pendingDifficulty = null; @@ -258,16 +285,28 @@ StratumClient.prototype.__proto__ = events.EventEmitter.prototype; * - 'client.disconnected'(StratumClientInstance) - when a miner disconnects. Be aware that the socket cannot be used anymore. * - 'started' - when the server is up and running **/ -var StratumServer = exports.Server = function StratumServer(ports, connectionTimeout, authorizeFn){ +var StratumServer = exports.Server = function StratumServer(ports, connectionTimeout, banning, authorizeFn){ //private members - var connectionTimeoutSeconds = connectionTimeout * 1000; + var socketTimeout = connectionTimeout * 1000; + var bannedMS = banning.time * 1000; var _this = this; var stratumClients = {}; var subscriptionCounter = SubscriptionCounter(); + var bannedIPs = {}; + + //Every 5 minutes look through bannedIPs for old bans and remove them in order to prevent a memory leak + var purgeOldBans = !banning.enabled ? null : setInterval(function(){ + for (ip in bannedIPs){ + var banTime = bannedIPs[ip]; + if (Date.now() - banTime > banning.time) + delete bannedIPs[ip]; + } + }, 1000 * 60 * 5); //5 minutes + var handleNewClient = function (socket) { socket.setKeepAlive(true); var subscriptionId = subscriptionCounter.next(); @@ -275,7 +314,9 @@ var StratumServer = exports.Server = function StratumServer(ports, connectionTim { subscriptionId : subscriptionId, socket : socket, - authorizeFn : authorizeFn + authorizeFn : authorizeFn, + banning : banning, + socketTimeout : socketTimeout } ); stratumClients[subscriptionId] = client; @@ -283,6 +324,8 @@ var StratumServer = exports.Server = function StratumServer(ports, connectionTim client.on('socketDisconnect', function() { _this.removeStratumClientBySubId(subscriptionId); _this.emit('client.disconnected', client); + }).on('ban', function(ipAddress){ + _this.banIP(ipAddress); }); return subscriptionId; }; @@ -291,6 +334,18 @@ var StratumServer = exports.Server = function StratumServer(ports, connectionTim Object.keys(ports).forEach(function(port){ net.createServer({allowHalfOpen: true}, function(socket){ + + if (banning.enabled && socket.remoteAddress in bannedIPs){ + var bannedTime = bannedIPs[socket.remoteAddress]; + if ((Date.now() - bannedTime) < bannedMS){ + socket.end(); + return; + } + else{ + delete bannedIPs[socket.remoteAddress]; + } + } + handleNewClient(socket); }).listen(parseInt(port), function(){ _this.emit('started'); @@ -302,16 +357,16 @@ var StratumServer = exports.Server = function StratumServer(ports, connectionTim //public members + this.banIP = function(ipAddress){ + bannedIPs[ipAddress] = Date.now(); + }; + this.broadcastMiningJobs = function(jobParams) { for (var clientId in stratumClients) { // if a client gets disconnected WHILE doing this loop a crash might happen. - // 'm not sure if that can ever happn but an if here doesn't hurt! + // 'm not sure if that can ever happen but an if here doesn't hurt! var client = stratumClients[clientId]; if (typeof(client) !== 'undefined') { - if (Date.now() - client.lastActivity > connectionTimeoutSeconds){ - client.socket.end(); - return; - } client.sendMiningJob(jobParams); } }