Skip to content

Commit b691f2d

Browse files
committed
Replace automatic bans with discouragement filter
This patch improves performance and resource usage around IP addresses that are banned for misbehavior. They're already not actually banned, as connections from them are still allowed, but they are preferred for eviction if the inbound connection slots are full. Stop treating these like manually banned IP ranges, and instead just keep them in a rolling Bloom filter of misbehaving nodes, which isn't persisted to disk or exposed through the ban framework. The effect remains the same: preferred for eviction, avoided for outgoing connections, and not relayed to other peers. Also change the name of this mechanism to better reflect reality; they're not banned, just discouraged. Contains release notes and several interface improvements by John Newbery.
1 parent 3276c14 commit b691f2d

File tree

9 files changed

+91
-44
lines changed

9 files changed

+91
-44
lines changed

doc/release-notes-19219.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#### Changes regarding misbehaving peers
2+
3+
Peers that misbehave (e.g. send us invalid blocks) are now referred to as
4+
discouraged nodes in log output, as they're not (and weren't) strictly banned:
5+
incoming connections are still allowed from them, but they're preferred for
6+
eviction.
7+
8+
Furthermore, a few additional changes are introduced to how discouraged
9+
addresses are treated:
10+
11+
- Discouraging an address does not time out automatically after 24 hours
12+
(or the `-bantime` setting). Depending on traffic from other peers,
13+
discouragement may time out at an indeterminate time.
14+
15+
- Discouragement is not persisted over restarts.
16+
17+
- There is no method to list discouraged addresses. They are not returned by
18+
the `listbanned` RPC. That RPC also no longer reports the `ban_reason`
19+
field, as `"manually added"` is the only remaining option.
20+
21+
- Discouragement cannot be removed with the `setban remove` RPC command.
22+
If you need to remove a discouragement, you can remove all discouragements by
23+
stop-starting your node.

src/banman.cpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,25 +74,24 @@ int BanMan::IsBannedLevel(CNetAddr net_addr)
7474
// 0 - Not banned
7575
// 1 - Automatic misbehavior ban
7676
// 2 - Any other ban
77-
int level = 0;
7877
auto current_time = GetTime();
7978
LOCK(m_cs_banned);
8079
for (const auto& it : m_banned) {
8180
CSubNet sub_net = it.first;
8281
CBanEntry ban_entry = it.second;
8382

8483
if (current_time < ban_entry.nBanUntil && sub_net.Match(net_addr)) {
85-
if (ban_entry.banReason != BanReasonNodeMisbehaving) return 2;
86-
level = 1;
84+
return 2;
8785
}
8886
}
89-
return level;
87+
return m_discouraged.contains(net_addr.GetAddrBytes()) ? 1 : 0;
9088
}
9189

9290
bool BanMan::IsBanned(CNetAddr net_addr)
9391
{
9492
auto current_time = GetTime();
9593
LOCK(m_cs_banned);
94+
if (m_discouraged.contains(net_addr.GetAddrBytes())) return true;
9695
for (const auto& it : m_banned) {
9796
CSubNet sub_net = it.first;
9897
CBanEntry ban_entry = it.second;
@@ -120,12 +119,18 @@ bool BanMan::IsBanned(CSubNet sub_net)
120119

121120
void BanMan::Ban(const CNetAddr& net_addr, const BanReason& ban_reason, int64_t ban_time_offset, bool since_unix_epoch)
122121
{
122+
if (ban_reason == BanReasonNodeMisbehaving) {
123+
LOCK(m_cs_banned);
124+
m_discouraged.insert(net_addr.GetAddrBytes());
125+
return;
126+
}
123127
CSubNet sub_net(net_addr);
124128
Ban(sub_net, ban_reason, ban_time_offset, since_unix_epoch);
125129
}
126130

127131
void BanMan::Ban(const CSubNet& sub_net, const BanReason& ban_reason, int64_t ban_time_offset, bool since_unix_epoch)
128132
{
133+
assert(ban_reason == BanReasonManuallyAdded);
129134
CBanEntry ban_entry(GetTime(), ban_reason);
130135

131136
int64_t normalized_ban_time_offset = ban_time_offset;

src/banman.h

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define BITCOIN_BANMAN_H
77

88
#include <addrdb.h>
9+
#include <bloom.h>
910
#include <fs.h>
1011
#include <net_types.h> // For banmap_t
1112
#include <sync.h>
@@ -23,20 +24,35 @@ class CClientUIInterface;
2324
class CNetAddr;
2425
class CSubNet;
2526

26-
// Denial-of-service detection/prevention
27-
// The idea is to detect peers that are behaving
28-
// badly and disconnect/ban them, but do it in a
29-
// one-coding-mistake-won't-shatter-the-entire-network
30-
// way.
31-
// IMPORTANT: There should be nothing I can give a
32-
// node that it will forward on that will make that
33-
// node's peers drop it. If there is, an attacker
34-
// can isolate a node and/or try to split the network.
35-
// Dropping a node for sending stuff that is invalid
36-
// now but might be valid in a later version is also
37-
// dangerous, because it can cause a network split
38-
// between nodes running old code and nodes running
39-
// new code.
27+
// Banman manages two related but distinct concepts:
28+
//
29+
// 1. Banning. This is configured manually by the user, through the setban RPC.
30+
// If an address or subnet is banned, we never accept incoming connections from
31+
// it and never create outgoing connections to it. We won't gossip its address
32+
// to other peers in addr messages. Banned addresses and subnets are stored to
33+
// banlist.dat on shutdown and reloaded on startup. Banning can be used to
34+
// prevent connections with spy nodes or other griefers.
35+
//
36+
// 2. Discouragement. If a peer misbehaves enough (see Misbehaving() in
37+
// net_processing.cpp), we'll mark that address as discouraged. We still allow
38+
// incoming connections from them, but they're preferred for eviction when
39+
// we receive new incoming connections. We never make outgoing connections to
40+
// them, and do not gossip their address to other peers. This is implemented as
41+
// a bloom filter. We can (probabilistically) test for membership, but can't
42+
// list all discouraged addresses or unmark them as discouraged. Discouragement
43+
// can prevent our limited connection slots being used up by incompatible
44+
// or broken peers.
45+
//
46+
// Neither banning nor discouragement are protections against denial-of-service
47+
// attacks, since if an attacker has a way to waste our resources and we
48+
// disconnect from them and ban that address, it's trivial for them to
49+
// reconnect from another IP address.
50+
//
51+
// Attempting to automatically disconnect or ban any class of peer carries the
52+
// risk of splitting the network. For example, if we banned/disconnected for a
53+
// transaction that fails a policy check and a future version changes the
54+
// policy check so the transaction is accepted, then that transaction could
55+
// cause the network to split between old nodes and new nodes.
4056

4157
class BanMan
4258
{
@@ -68,6 +84,7 @@ class BanMan
6884
CClientUIInterface* m_client_interface = nullptr;
6985
CBanDB m_ban_db;
7086
const int64_t m_default_ban_time;
87+
CRollingBloomFilter m_discouraged GUARDED_BY(m_cs_banned) {50000, 0.000001};
7188
};
7289

7390
#endif

src/init.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,8 @@ void SetupServerArgs(NodeContext& node)
430430

431431
gArgs.AddArg("-addnode=<ip>", "Add a node to connect to and attempt to keep the connection open (see the `addnode` RPC command help for more info). This option can be specified multiple times to add multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION);
432432
gArgs.AddArg("-asmap=<file>", strprintf("Specify asn mapping used for bucketing of the peers (default: %s). Relative paths will be prefixed by the net-specific datadir location.", DEFAULT_ASMAP_FILENAME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
433-
gArgs.AddArg("-banscore=<n>", strprintf("Threshold for disconnecting misbehaving peers (default: %u)", DEFAULT_BANSCORE_THRESHOLD), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
434-
gArgs.AddArg("-bantime=<n>", strprintf("Number of seconds to keep misbehaving peers from reconnecting (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
433+
gArgs.AddArg("-banscore=<n>", strprintf("Threshold for disconnecting and discouraging misbehaving peers (default: %u)", DEFAULT_BANSCORE_THRESHOLD), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
434+
gArgs.AddArg("-bantime=<n>", strprintf("Default duration (in seconds) of manually configured bans (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
435435
gArgs.AddArg("-bind=<addr>", "Bind to given address and always listen on it. Use [host]:port notation for IPv6", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION);
436436
gArgs.AddArg("-connect=<ip>", "Connect only to the specified node; -noconnect disables automatic connections (the rules for this peer are the same as for -addnode). This option can be specified multiple times to connect to multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION);
437437
gArgs.AddArg("-discover", "Discover own IP addresses (default: 1 when listening and no -externalip or -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);

src/net.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,15 +1012,22 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) {
10121012

10131013
int bannedlevel = m_banman ? m_banman->IsBannedLevel(addr) : 0;
10141014

1015-
// Don't accept connections from banned peers, but if our inbound slots aren't almost full, accept
1016-
// if the only banning reason was an automatic misbehavior ban.
1017-
if (!NetPermissions::HasFlag(permissionFlags, NetPermissionFlags::PF_NOBAN) && bannedlevel > ((nInbound + 1 < nMaxInbound) ? 1 : 0))
1015+
// Don't accept connections from banned peers.
1016+
if (!NetPermissions::HasFlag(permissionFlags, NetPermissionFlags::PF_NOBAN) && bannedlevel == 2)
10181017
{
10191018
LogPrint(BCLog::NET, "connection from %s dropped (banned)\n", addr.ToString());
10201019
CloseSocket(hSocket);
10211020
return;
10221021
}
10231022

1023+
// Only accept connections from discouraged peers if our inbound slots aren't (almost) full.
1024+
if (!NetPermissions::HasFlag(permissionFlags, NetPermissionFlags::PF_NOBAN) && nInbound + 1 >= nMaxInbound && bannedlevel >= 1)
1025+
{
1026+
LogPrint(BCLog::NET, "connection from %s dropped (discouraged)\n", addr.ToString());
1027+
CloseSocket(hSocket);
1028+
return;
1029+
}
1030+
10241031
if (nInbound >= nMaxInbound)
10251032
{
10261033
if (!AttemptToEvictConnection()) {

src/net_processing.cpp

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,7 +1019,7 @@ unsigned int LimitOrphanTxSize(unsigned int nMaxOrphans)
10191019
}
10201020

10211021
/**
1022-
* Mark a misbehaving peer to be banned depending upon the value of `-banscore`.
1022+
* Increment peer's misbehavior score. If the new value surpasses banscore (specified on startup or by default), mark node to be discouraged, meaning the peer might be disconnected & added to the discouragement filter.
10231023
*/
10241024
void Misbehaving(NodeId pnode, int howmuch, const std::string& message) EXCLUSIVE_LOCKS_REQUIRED(cs_main)
10251025
{
@@ -1035,14 +1035,14 @@ void Misbehaving(NodeId pnode, int howmuch, const std::string& message) EXCLUSIV
10351035
std::string message_prefixed = message.empty() ? "" : (": " + message);
10361036
if (state->nMisbehavior >= banscore && state->nMisbehavior - howmuch < banscore)
10371037
{
1038-
LogPrint(BCLog::NET, "%s: %s peer=%d (%d -> %d) BAN THRESHOLD EXCEEDED%s\n", __func__, state->name, pnode, state->nMisbehavior-howmuch, state->nMisbehavior, message_prefixed);
1038+
LogPrint(BCLog::NET, "%s: %s peer=%d (%d -> %d) DISCOURAGE THRESHOLD EXCEEDED%s\n", __func__, state->name, pnode, state->nMisbehavior-howmuch, state->nMisbehavior, message_prefixed);
10391039
state->fShouldBan = true;
10401040
} else
10411041
LogPrint(BCLog::NET, "%s: %s peer=%d (%d -> %d)%s\n", __func__, state->name, pnode, state->nMisbehavior-howmuch, state->nMisbehavior, message_prefixed);
10421042
}
10431043

10441044
/**
1045-
* Potentially ban a node based on the contents of a BlockValidationState object
1045+
* Potentially mark a node discouraged based on the contents of a BlockValidationState object
10461046
*
10471047
* @param[in] via_compact_block this bool is passed in because net_processing should
10481048
* punish peers differently depending on whether the data was provided in a compact
@@ -1072,7 +1072,7 @@ static bool MaybePunishNodeForBlock(NodeId nodeid, const BlockValidationState& s
10721072
break;
10731073
}
10741074

1075-
// Ban outbound (but not inbound) peers if on an invalid chain.
1075+
// Discourage outbound (but not inbound) peers if on an invalid chain.
10761076
// Exempt HB compact block peers and manual connections.
10771077
if (!via_compact_block && !node_state->m_is_inbound && !node_state->m_is_manual_connection) {
10781078
Misbehaving(nodeid, 100, message);
@@ -1107,7 +1107,7 @@ static bool MaybePunishNodeForBlock(NodeId nodeid, const BlockValidationState& s
11071107
}
11081108

11091109
/**
1110-
* Potentially ban a node based on the contents of a TxValidationState object
1110+
* Potentially disconnect and discourage a node based on the contents of a TxValidationState object
11111111
*
11121112
* @return Returns true if the peer was punished (probably disconnected)
11131113
*/
@@ -1339,7 +1339,7 @@ void PeerLogicValidation::UpdatedBlockTip(const CBlockIndex *pindexNew, const CB
13391339
}
13401340

13411341
/**
1342-
* Handle invalid block rejection and consequent peer banning, maintain which
1342+
* Handle invalid block rejection and consequent peer discouragement, maintain which
13431343
* peers announce compact blocks.
13441344
*/
13451345
void PeerLogicValidation::BlockChecked(const CBlock& block, const BlockValidationState& state) {
@@ -3118,7 +3118,7 @@ void ProcessMessage(
31183118
// relayed before full validation (see BIP 152), so we don't want to disconnect
31193119
// the peer if the header turns out to be for an invalid block.
31203120
// Note that if a peer tries to build on an invalid chain, that
3121-
// will be detected and the peer will be banned.
3121+
// will be detected and the peer will be disconnected/discouraged.
31223122
return ProcessHeadersMessage(pfrom, connman, chainman, mempool, {cmpctblock.header}, chainparams, /*via_compact_block=*/true);
31233123
}
31243124

@@ -3204,7 +3204,7 @@ void ProcessMessage(
32043204
// 3. the block is otherwise invalid (eg invalid coinbase,
32053205
// block is too big, too many legacy sigops, etc).
32063206
// So if CheckBlock failed, #3 is the only possibility.
3207-
// Under BIP 152, we don't DoS-ban unless proof of work is
3207+
// Under BIP 152, we don't discourage the peer unless proof of work is
32083208
// invalid (we don't require all the stateless checks to have
32093209
// been run). This is handled below, so just treat this as
32103210
// though the block was successfully read, and rely on the
@@ -3576,11 +3576,12 @@ bool PeerLogicValidation::CheckIfBanned(CNode& pnode)
35763576
else if (pnode.m_manual_connection)
35773577
LogPrintf("Warning: not punishing manually-connected peer %s!\n", pnode.addr.ToString());
35783578
else if (pnode.addr.IsLocal()) {
3579-
// Disconnect but don't ban _this_ local node
3580-
LogPrintf("Warning: disconnecting but not banning local peer %s!\n", pnode.addr.ToString());
3579+
// Disconnect but don't discourage this local node
3580+
LogPrintf("Warning: disconnecting but not discouraging local peer %s!\n", pnode.addr.ToString());
35813581
pnode.fDisconnect = true;
35823582
} else {
35833583
// Disconnect and ban all nodes sharing the address
3584+
LogPrintf("Disconnecting and discouraging peer %s!\n", pnode.addr.ToString());
35843585
if (m_banman) {
35853586
m_banman->Ban(pnode.addr, BanReasonNodeMisbehaving);
35863587
}

src/netaddress.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class CNetAddr
9090
uint32_t GetMappedAS(const std::vector<bool> &asmap) const;
9191

9292
std::vector<unsigned char> GetGroup(const std::vector<bool> &asmap) const;
93+
std::vector<unsigned char> GetAddrBytes() const { return {std::begin(ip), std::end(ip)}; }
9394
int GetReachabilityFrom(const CNetAddr *paddrPartner = nullptr) const;
9495

9596
explicit CNetAddr(const struct in6_addr& pipv6Addr, const uint32_t scope = 0);

src/rpc/net.cpp

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,8 @@ static UniValue setban(const JSONRPCRequest& request)
601601

602602
if (strCommand == "add")
603603
{
604-
if (isSubnet ? node.banman->IsBanned(subNet) : node.banman->IsBanned(netAddr)) {
604+
if ((isSubnet && node.banman->IsBanned(subNet)) ||
605+
(!isSubnet && node.banman->IsBannedLevel(netAddr) == BanReasonManuallyAdded)) {
605606
throw JSONRPCError(RPC_CLIENT_NODE_ALREADY_ADDED, "Error: IP/Subnet already banned");
606607
}
607608

@@ -628,7 +629,7 @@ static UniValue setban(const JSONRPCRequest& request)
628629
else if(strCommand == "remove")
629630
{
630631
if (!( isSubnet ? node.banman->Unban(subNet) : node.banman->Unban(netAddr) )) {
631-
throw JSONRPCError(RPC_CLIENT_INVALID_IP_OR_SUBNET, "Error: Unban failed. Requested address/subnet was not previously banned.");
632+
throw JSONRPCError(RPC_CLIENT_INVALID_IP_OR_SUBNET, "Error: Unban failed. Requested address/subnet was not previously manually banned.");
632633
}
633634
}
634635
return NullUniValue;
@@ -637,7 +638,7 @@ static UniValue setban(const JSONRPCRequest& request)
637638
static UniValue listbanned(const JSONRPCRequest& request)
638639
{
639640
RPCHelpMan{"listbanned",
640-
"\nList all banned IPs/Subnets.\n",
641+
"\nList all manually banned IPs/Subnets.\n",
641642
{},
642643
RPCResult{RPCResult::Type::ARR, "", "",
643644
{
@@ -646,7 +647,6 @@ static UniValue listbanned(const JSONRPCRequest& request)
646647
{RPCResult::Type::STR, "address", ""},
647648
{RPCResult::Type::NUM_TIME, "banned_until", ""},
648649
{RPCResult::Type::NUM_TIME, "ban_created", ""},
649-
{RPCResult::Type::STR, "ban_reason", ""},
650650
}},
651651
}},
652652
RPCExamples{
@@ -671,7 +671,6 @@ static UniValue listbanned(const JSONRPCRequest& request)
671671
rec.pushKV("address", entry.first.ToString());
672672
rec.pushKV("banned_until", banEntry.nBanUntil);
673673
rec.pushKV("ban_created", banEntry.nCreateTime);
674-
rec.pushKV("ban_reason", banEntry.banReasonToString());
675674

676675
bannedAddresses.push_back(rec);
677676
}

src/test/denialofservice_tests.cpp

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -346,12 +346,6 @@ BOOST_AUTO_TEST_CASE(DoS_bantime)
346346
}
347347
BOOST_CHECK(banman->IsBanned(addr));
348348

349-
SetMockTime(nStartTime+60*60);
350-
BOOST_CHECK(banman->IsBanned(addr));
351-
352-
SetMockTime(nStartTime+60*60*24+1);
353-
BOOST_CHECK(!banman->IsBanned(addr));
354-
355349
bool dummy;
356350
peerLogic->FinalizeNode(dummyNode.GetId(), dummy);
357351
}

0 commit comments

Comments
 (0)