From 47af5987aec2f263bf16c213fb15ed7b484a2e29 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Wed, 20 Jan 2016 02:46:25 -0800 Subject: [PATCH] better system to prevent DOSing. --- lib/bcoin/block.js | 2 +- lib/bcoin/chain.js | 42 +++---- lib/bcoin/peer.js | 6 +- lib/bcoin/pool.js | 194 ++++++++++++++++++++++++-------- lib/bcoin/protocol/constants.js | 3 + lib/bcoin/protocol/network.js | 69 ++++++------ 6 files changed, 203 insertions(+), 113 deletions(-) diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 22084438..3a9c40d1 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -489,7 +489,7 @@ Block.prototype.verifyContext = function verifyContext() { }; Block.prototype.isGenesis = function isGenesis() { - return this.hash('hex') === utils.toHex(network.genesis._hash); + return this.hash('hex') === network.genesis.hash; }; Block.prototype.getHeight = function getHeight() { diff --git a/lib/bcoin/chain.js b/lib/bcoin/chain.js index fcf8f9a0..76592b8d 100644 --- a/lib/bcoin/chain.js +++ b/lib/bcoin/chain.js @@ -56,9 +56,9 @@ function Chain(options) { network: network.type, entries: [ { - hash: utils.toHex(network.genesis._hash), + hash: network.genesis.hash, version: network.genesis.version, - prevBlock: utils.toHex(network.genesis.prevBlock), + prevBlock: network.genesis.prevBlock, ts: network.genesis.ts, bits: network.genesis.bits, height: 0 @@ -302,9 +302,8 @@ Chain.prototype.add = function add(block, peer) { code = Chain.codes.invalid; this.emit('invalid', { height: prevHeight + 1, - hash: hash, - peer: peer - }); + hash: hash + }, peer); break; } @@ -322,9 +321,8 @@ Chain.prototype.add = function add(block, peer) { height: -1, expected: this.orphan.map[prevHash].hash('hex'), received: hash, - checkpoint: null, - peer: peer - }); + checkpoint: null + }, peer); code = Chain.codes.forked; break; } @@ -383,9 +381,8 @@ Chain.prototype.add = function add(block, peer) { height: prevHeight + 1, expected: tip.hash, received: hash, - checkpoint: null, - peer: peer - }); + checkpoint: null + }, peer); code = Chain.codes.forked; break; } @@ -397,9 +394,8 @@ Chain.prototype.add = function add(block, peer) { code = Chain.codes.invalid; this.emit('invalid', { height: prevHeight + 1, - hash: hash, - peer: peer - }); + hash: hash + }, peer); break; } @@ -422,8 +418,7 @@ Chain.prototype.add = function add(block, peer) { height: entry.height, expected: network.checkpoints[entry.height], received: entry.hash, - checkpoint: true, - peer: peer + checkpoint: true }); break; } @@ -483,17 +478,14 @@ Chain.prototype.add = function add(block, peer) { return total; }; -Chain.prototype.has = function has(hash, cb) { - if (this.loading) { - this.once('load', function() { - this.has(hash, noIndex, cb); - }); - return; - } +Chain.prototype.has = function has(hash) { + if (this.hasBlock(hash)) + return true; - cb = utils.asyncify(cb); + if (this.hasOrphan(hash)) + return true; - return cb(this.hasBlock(hash) || this.hasOrphan(hash)); + return false; }; Chain.prototype.byHeight = function byHeight(height) { diff --git a/lib/bcoin/peer.js b/lib/bcoin/peer.js index 9cf140ce..0ad5742d 100644 --- a/lib/bcoin/peer.js +++ b/lib/bcoin/peer.js @@ -113,7 +113,7 @@ Peer.prototype._init = function init() { this.socket.once('error', function(err) { self._error(err); - self.emit('misbehave'); + self.pool.misbehaving(self, 100); }); this.socket.once('close', function() { @@ -134,7 +134,7 @@ Peer.prototype._init = function init() { // Something is wrong here. // Ignore this peer. self.destroy(); - self.emit('misbehave'); + self.pool.misbehaving(self, 100); }); if (this.pool.options.fullNode) { @@ -156,7 +156,7 @@ Peer.prototype._init = function init() { if (err) { self._error(err); self.destroy(); - self.emit('misbehave'); + self.pool.misbehaving(self, 100); return; } self.ack = true; diff --git a/lib/bcoin/pool.js b/lib/bcoin/pool.js index b423422c..28b7b465 100644 --- a/lib/bcoin/pool.js +++ b/lib/bcoin/pool.js @@ -12,6 +12,7 @@ var bcoin = require('../bcoin'); var utils = bcoin.utils; var assert = utils.assert; var network = bcoin.protocol.network; +var constants = bcoin.protocol.constants; /** * Pool @@ -40,9 +41,6 @@ function Pool(options) { this.originalSeeds = (options.seeds || network.seeds).map(utils.parseHost); this.setSeeds([]); - this._priorityTries = {}; - this._regularTries = {}; - this._misbehaving = {}; this.storage = this.options.storage; this.destroyed = false; @@ -112,13 +110,21 @@ function Pool(options) { // Peers that are loading block ids load: null, // All peers - all: [] + all: [], + // Misbehaving hosts + misbehaving: {}, + // Attempts at using seed peers + tries: { + priority: {}, + regular: {} + } }; this.block = { bestHeight: 0, bestHash: null, - type: this.options.fullNode ? 'block' : 'filtered' + type: this.options.fullNode ? 'block' : 'filtered', + invalid: {} }; this.request = { @@ -179,36 +185,36 @@ Pool.prototype._init = function _init() { self.emit('block', block, peer); }); - this.chain.on('fork', function(data) { + this.chain.on('fork', function(data, peer) { self.emit('debug', 'Fork at height %d: expected=%s received=%s checkpoint=%s peer=%s', data.height, utils.revHex(data.expected), utils.revHex(data.received), data.checkpoint, - data.peer ? data.peer.host : '' + peer ? peer.host : '' ); - if (!data.peer) + if (!peer) return; - data.peer.destroy(); + self.misbehaving(peer, 100); }); - this.chain.on('invalid', function(data) { + this.chain.on('invalid', function(data, peer) { self.emit('debug', 'Invalid block at height: %d: hash=%s peer=%s', data.height, utils.revHex(data.hash), - data.peer ? data.peer.host : '' + peer ? peer.host : '' ); - if (!data.peer) + self.block.invalid[data.hash] = true; + + if (!peer) return; - // We should technically use a ban score - // here instead of killing the peer. - data.peer.destroy(); + self.misbehaving(peer, 100); }); this.options.wallets.forEach(function(w) { @@ -536,10 +542,12 @@ Pool.prototype._handleInv = function _handleInv(hashes, peer) { Pool.prototype._handleBlock = function _handleBlock(block, peer) { var self = this; + var requested, hasPrev; - var requested = this._response(block); + // Fulfill our request. + requested = this._response(block); - // Emulate bip37 - emit all the "watched" txs + // Emulate BIP37: emit all the filtered transactions. if (this.options.fullNode && this.listeners('watched').length > 0) { block.txs.forEach(function(tx) { if (self.isWatched(tx)) @@ -547,21 +555,39 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer) { }); } - // Ignore if we already have - if (this.chain.has(block)) { - this.emit('debug', 'Already have block %s (%s)', block.height, peer.host); + // Ensure the block was not invalid last time. + // Someone might be sending us bad blocks to DoS us. + if (this.block.invalid[block.hash('hex')]) { + this.misbehaving(peer, 100); return false; } - // Make sure the block is valid + // Ensure this is not a continuation + // of an invalid chain. + if (this.block.invalid[block.prevBlock]) { + this.misbehaving(peer, 100); + return false; + } + + // Ignore if we already have. + if (this.chain.has(block)) { + this.emit('debug', 'Already have block %s (%s)', block.height, peer.host); + this.misbehaving(peer, 1); + return false; + } + + // Make sure the block is valid. if (!block.verify()) { this.emit('debug', 'Block verification failed for %s (%s)', block.rhash, peer.host); + this.block.invalid[block.hash('hex')] = true; + this.misbehaving(peer, 100); return false; } - // Someone is sending us blocks without us requesting them. + // Someone is sending us blocks without + // us requesting them. if (!requested) { this.emit('debug', 'Recieved unrequested block: %s (%s)', @@ -571,19 +597,40 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer) { // Resolve orphan chain if (!this.options.headers) { if (!this.chain.hasBlock(block.prevBlock)) { + // Special case for genesis block. + if (block.isGenesis()) + return false; + + // Make sure the peer doesn't send us + // more than 100 orphans every 3 minutes. + if (this.orphaning(peer)) { + this.misbehaving(peer, 100); + return false; + } + // NOTE: If we were to emit new orphans here, we // would not need to store full blocks as orphans. // However, the listener would not be able to see // the height until later. if (this._addIndex(block, peer)) this.emit('pool block', block, peer); + this.peers.load.loadBlocks( this.chain.locatorHashes(), this.chain.getOrphanRoot(block) ); + this.emit('debug', 'Handled orphan %s (%s)', block.rhash, peer.host); + return false; } + } else { + if (!this.chain.hasBlock(block.prevBlock)) { + if (!this.options.multiplePeers) { + if (this.misbehaving(peer, 10)) + return false; + } + } } // Add to index and emit/save @@ -688,10 +735,6 @@ Pool.prototype._createPeer = function _createPeer(backoff, priority) { self.emit.apply(self, ['debug'].concat(args)); }); - peer.once('misbehave', function() { - self._misbehaving[peer.host] = true; - }); - peer.on('reject', function(payload) { var data = utils.revHex(utils.toHex(payload.data)); @@ -1203,37 +1246,31 @@ Pool.prototype.search = function search(id, range, e) { }; Pool.prototype._request = function _request(type, hash, options, cb) { - var self = this; - - // Optional `force` if (typeof options === 'function') { cb = options; options = {}; } + if (!options) options = {}; hash = utils.toHex(hash); + if (this.request.map[hash]) return this.request.map[hash].addCallback(cb); - function next(has) { - if (has) - return; - - if (self.destroyed) - return; - - var req = new LoadRequest(self, type, hash, cb); - req.add(options.noQueue); - } - // Block should be not in chain, or be requested // Do not use with headers-first - if (!options.force && (type === 'block' || type === 'filtered')) - return this.chain.has(hash, next); + if (!options.force && (type === 'block' || type === 'filtered')) { + if (this.chain.has(hash)) + return; + } - return next(false); + if (this.destroyed) + return; + + var req = new LoadRequest(this, type, hash, cb); + req.add(options.noQueue); }; Pool.prototype._response = function _response(entity) { @@ -1498,7 +1535,10 @@ Pool.prototype.usableSeed = function usableSeed(priority, connecting) { var i, addr; var original = this.originalSeeds; var seeds = this.seeds; - var tries = priority ? this._priorityTries : this._regularTries; + var tries = this.peers.tries.regular; + + if (priority) + tries = this.peers.tries.priority; // Hang back if we don't have a loader peer yet. if (!connecting && !priority && (!this.peers.load || !this.peers.load.socket)) @@ -1516,7 +1556,7 @@ Pool.prototype.usableSeed = function usableSeed(priority, connecting) { assert(addr.host); if (this.getPeer(addr)) continue; - if (this._misbehaving[addr.host]) + if (this.isMisbehaving(addr.host)) continue; if (tries[addr.host]) continue; @@ -1533,7 +1573,7 @@ Pool.prototype.usableSeed = function usableSeed(priority, connecting) { assert(addr.host); if (this.peers.load && this.getPeer(addr) === this.peers.load) continue; - if (this._misbehaving[addr.host]) + if (this.isMisbehaving(addr.host)) continue; if (tries[addr.host]) continue; @@ -1549,7 +1589,7 @@ Pool.prototype.usableSeed = function usableSeed(priority, connecting) { assert(addr.host); if (this.getPeer(addr)) continue; - if (this._misbehaving[addr.host]) + if (this.isMisbehaving(addr.host)) continue; return addr; } @@ -1562,7 +1602,7 @@ Pool.prototype.usableSeed = function usableSeed(priority, connecting) { assert(addr.host); if (this.peers.load && this.getPeer(addr) === this.peers.load) continue; - if (this._misbehaving[addr.host]) + if (this.isMisbehaving(addr.host)) continue; return addr; } @@ -1622,6 +1662,64 @@ Pool.prototype.removeSeed = function removeSeed(seed) { return true; }; +Pool.prototype.orphaning = function orphaning(peer) { + if (!peer._orphanTime) + peer._orphanTime = utils.now(); + + if (!peer._orphans) + peer._orphans = 0; + + if (utils.now() > peer._orphanTime + 3 * 60) { + peer._orphans = 0; + peer._orphanTime = utils.now(); + } + + peer._orphans += 1; + + if (peer._orphans > 100) + return true; + + return false; +}; + +Pool.prototype.misbehaving = function misbehaving(peer, dos) { + if (!peer._banscore) + peer._banscore = 0; + + peer._banscore += dos; + + if (peer._banscore >= constants.banScore) { + this.peers.misbehaving[peer.host] = utils.now(); + this.emit('debug', 'Ban threshold exceeded for %s', peer.host); + peer.destroy(); + return true; + } + + return false; +}; + +Pool.prototype.isMisbehaving = function isMisbehaving(host) { + var peer, time; + + if (host.host) + host = host.host; + + time = this.peers.misbehaving[host]; + + if (time) { + if (utils.now() > time + constants.banTime) { + delete this.peers.misbehaving[host]; + peer = this.getPeer(host); + if (peer) + peer._banscore = 0; + return false; + } + return true; + } + + return false; +}; + Pool.prototype.toJSON = function toJSON() { return { v: 1, diff --git a/lib/bcoin/protocol/constants.js b/lib/bcoin/protocol/constants.js index 2c493b4e..8f3012e3 100644 --- a/lib/bcoin/protocol/constants.js +++ b/lib/bcoin/protocol/constants.js @@ -262,3 +262,6 @@ exports.userAgent = '/bcoin:' + exports.userVersion + '/'; exports.coin = new bn(10000000).muln(10); exports.cent = new bn(1000000); exports.maxMoney = new bn(21000000).mul(exports.coin); + +exports.banTime = 24 * 60 * 60; +exports.banScore = 100; diff --git a/lib/bcoin/protocol/network.js b/lib/bcoin/protocol/network.js index da44202b..2a257543 100644 --- a/lib/bcoin/protocol/network.js +++ b/lib/bcoin/protocol/network.js @@ -88,18 +88,17 @@ main.halvingInterval = 210000; // http://blockexplorer.com/rawblock/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f main.genesis = { version: 1, - _hash: utils.toArray( - '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', - 'hex' - ).reverse(), - prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0, + hash: utils.revHex( + '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' + ), + prevBlock: utils.toHex( + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 ], - merkleRoot: utils.toArray( - '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', - 'hex' - ).reverse(), + 0, 0, 0, 0, 0, 0, 0, 0 ]), + merkleRoot: utils.revHex( + '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b' + ), ts: 1231006505, bits: 0x1d00ffff, nonce: 2083236893 @@ -113,9 +112,9 @@ main.preload = { network: main.type, entries: [ { - hash: utils.toHex(main.genesis._hash), + hash: main.genesis.hash, version: main.genesis.version, - prevBlock: utils.toHex(main.genesis.prevBlock), + prevBlock: main.genesis.prevBlock, ts: main.genesis.ts, bits: main.genesis.bits, height: 0 @@ -203,18 +202,17 @@ testnet.halvingInterval = 210000; // http://blockexplorer.com/testnet/rawblock/000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943 testnet.genesis = { version: 1, - _hash: utils.toArray( - '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943', - 'hex' - ).reverse(), - prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0, + hash: utils.revHex( + '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943' + ), + prevBlock: utils.toHex( + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 ], - merkleRoot: utils.toArray( - '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', - 'hex' - ).reverse(), + 0, 0, 0, 0, 0, 0, 0, 0 ]), + merkleRoot: utils.revHex( + '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b' + ), ts: 1296688602, bits: 0x1d00ffff, nonce: 414098458 @@ -228,9 +226,9 @@ testnet.preload = { network: testnet.type, entries: [ { - hash: utils.toHex(testnet.genesis._hash), + hash: testnet.genesis.hash, version: testnet.genesis.version, - prevBlock: utils.toHex(testnet.genesis.prevBlock), + prevBlock: testnet.genesis.prevBlock, ts: testnet.genesis.ts, bits: testnet.genesis.bits, height: 0 @@ -300,18 +298,17 @@ regtest.halvingInterval = 150; regtest.genesis = { version: 1, - _hash: utils.toArray( - '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206', - 'hex' - ).reverse(), - prevBlock: [ 0, 0, 0, 0, 0, 0, 0, 0, + hash: utils.revHex( + '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206' + ), + prevBlock: utils.toHex( + [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0 ], - merkleRoot: utils.toArray( - '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b', - 'hex' - ).reverse(), + 0, 0, 0, 0, 0, 0, 0, 0 ]), + merkleRoot: utils.revHex( + '4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b' + ), ts: 1296688602, bits: 0x207fffff, nonce: 2 @@ -325,9 +322,9 @@ regtest.preload = { network: regtest.type, entries: [ { - hash: utils.toHex(regtest.genesis._hash), + hash: regtest.genesis.hash, version: regtest.genesis.version, - prevBlock: utils.toHex(regtest.genesis.prevBlock), + prevBlock: regtest.genesis.prevBlock, ts: regtest.genesis.ts, bits: regtest.genesis.bits, height: 0