better system to prevent DOSing.

This commit is contained in:
Christopher Jeffrey 2016-01-20 02:46:25 -08:00
parent a2c08d9692
commit 47af5987ae
6 changed files with 203 additions and 113 deletions

View File

@ -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() {

View File

@ -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) {

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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