Skip to content

Commit d6e0d78

Browse files
author
MarcoFalke
committed
Merge bitcoin/bitcoin#20966: banman: save the banlist in a JSON format on disk
bb719a0 style: remove () from assert in rpc_setban.py (Vasil Dimov) 24b10eb doc: fix grammar in doc/files.md (Vasil Dimov) dd4e957 test: ensure banlist can be read from disk after restart (Vasil Dimov) d197977 banman: save the banlist in a JSON format on disk (Vasil Dimov) Pull request description: Save the banlist in `banlist.json` instead of `banlist.dat`. This makes it possible to store Tor v3 entries in the banlist on disk (and any other addresses that cannot be serialized in addrv1 format). Only read `banlist.dat` if it exists and `banlist.json` does not exist (first start after an upgrade). Supersedes bitcoin/bitcoin#20904 Resolves bitcoin/bitcoin#19748 ACKs for top commit: jonatack: Code review re-ACK bb719a0 per `git range-diff 6a67366 4b52c72 bb719a0` achow101: Code Review ACK bb719a0 Tree-SHA512: fc135c3a1fe20bcf5d008ce6bea251b4135e56c78bf8f750b4bd8144c095b81ffe165133cdc7e4715875eec7e7c4e13ad9f5d2450b21102af063d7c8abf716b6
2 parents 03aa59a + bb719a0 commit d6e0d78

File tree

10 files changed

+193
-49
lines changed

10 files changed

+193
-49
lines changed

doc/files.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ Subdirectory | File(s) | Description
5656
`indexes/coinstats/db/` | LevelDB database | Coinstats index; *optional*, used if `-coinstatsindex=1`
5757
`wallets/` | | [Contains wallets](#multi-wallet-environment); can be specified by `-walletdir` option; if `wallets/` subdirectory does not exist, wallets reside in the [data directory](#data-directory-location)
5858
`./` | `anchors.dat` | Anchor IP address database, created on shutdown and deleted at startup. Anchors are last known outgoing block-relay-only peers that are tried to re-connect to on startup
59-
`./` | `banlist.dat` | Stores the IPs/subnets of banned nodes
59+
`./` | `banlist.dat` | Stores the addresses/subnets of banned nodes (deprecated). `bitcoind` or `bitcoin-qt` no longer save the banlist to this file, but read it on startup if `banlist.json` is not present.
60+
`./` | `banlist.json` | Stores the addresses/subnets of banned nodes.
6061
`./` | `bitcoin.conf` | User-defined [configuration settings](bitcoin-conf.md) for `bitcoind` or `bitcoin-qt`. File is not written to by the software and must be created manually. Path can be specified by `-conf` option
6162
`./` | `bitcoind.pid` | Stores the process ID (PID) of `bitcoind` or `bitcoin-qt` while running; created at start and deleted on shutdown; can be specified by `-pid` option
6263
`./` | `debug.log` | Contains debug information and general logging generated by `bitcoind` or `bitcoin-qt`; can be specified by `-debuglogfile` option
@@ -109,7 +110,7 @@ Subdirectory | File | Description
109110

110111
## Legacy subdirectories and files
111112

112-
These subdirectories and files are no longer used by the Bitcoin Core:
113+
These subdirectories and files are no longer used by Bitcoin Core:
113114

114115
Path | Description | Repository notes
115116
---------------|-------------|-----------------

src/addrdb.cpp

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,72 @@
1111
#include <cstdint>
1212
#include <hash.h>
1313
#include <logging/timer.h>
14+
#include <netbase.h>
1415
#include <random.h>
1516
#include <streams.h>
1617
#include <tinyformat.h>
18+
#include <univalue.h>
19+
#include <util/settings.h>
1720
#include <util/system.h>
1821

22+
CBanEntry::CBanEntry(const UniValue& json)
23+
: nVersion(json["version"].get_int()), nCreateTime(json["ban_created"].get_int64()),
24+
nBanUntil(json["banned_until"].get_int64())
25+
{
26+
}
27+
28+
UniValue CBanEntry::ToJson() const
29+
{
30+
UniValue json(UniValue::VOBJ);
31+
json.pushKV("version", nVersion);
32+
json.pushKV("ban_created", nCreateTime);
33+
json.pushKV("banned_until", nBanUntil);
34+
return json;
35+
}
36+
1937
namespace {
2038

39+
static const char* BANMAN_JSON_ADDR_KEY = "address";
40+
41+
/**
42+
* Convert a `banmap_t` object to a JSON array.
43+
* @param[in] bans Bans list to convert.
44+
* @return a JSON array, similar to the one returned by the `listbanned` RPC. Suitable for
45+
* passing to `BanMapFromJson()`.
46+
*/
47+
UniValue BanMapToJson(const banmap_t& bans)
48+
{
49+
UniValue bans_json(UniValue::VARR);
50+
for (const auto& it : bans) {
51+
const auto& address = it.first;
52+
const auto& ban_entry = it.second;
53+
UniValue j = ban_entry.ToJson();
54+
j.pushKV(BANMAN_JSON_ADDR_KEY, address.ToString());
55+
bans_json.push_back(j);
56+
}
57+
return bans_json;
58+
}
59+
60+
/**
61+
* Convert a JSON array to a `banmap_t` object.
62+
* @param[in] bans_json JSON to convert, must be as returned by `BanMapToJson()`.
63+
* @param[out] bans Bans list to create from the JSON.
64+
* @throws std::runtime_error if the JSON does not have the expected fields or they contain
65+
* unparsable values.
66+
*/
67+
void BanMapFromJson(const UniValue& bans_json, banmap_t& bans)
68+
{
69+
for (const auto& ban_entry_json : bans_json.getValues()) {
70+
CSubNet subnet;
71+
const auto& subnet_str = ban_entry_json[BANMAN_JSON_ADDR_KEY].get_str();
72+
if (!LookupSubNet(subnet_str, subnet)) {
73+
throw std::runtime_error(
74+
strprintf("Cannot parse banned address or subnet: %s", subnet_str));
75+
}
76+
bans.insert_or_assign(subnet, CBanEntry{ban_entry_json});
77+
}
78+
}
79+
2180
template <typename Stream, typename Data>
2281
bool SerializeDB(Stream& stream, const Data& data)
2382
{
@@ -119,18 +178,54 @@ bool DeserializeFileDB(const fs::path& path, Data& data, int version)
119178
}
120179
} // namespace
121180

122-
CBanDB::CBanDB(fs::path ban_list_path) : m_ban_list_path(std::move(ban_list_path))
181+
CBanDB::CBanDB(fs::path ban_list_path)
182+
: m_banlist_dat(ban_list_path.string() + ".dat"),
183+
m_banlist_json(ban_list_path.string() + ".json")
123184
{
124185
}
125186

126187
bool CBanDB::Write(const banmap_t& banSet)
127188
{
128-
return SerializeFileDB("banlist", m_ban_list_path, banSet, CLIENT_VERSION);
189+
std::vector<std::string> errors;
190+
if (util::WriteSettings(m_banlist_json, {{JSON_KEY, BanMapToJson(banSet)}}, errors)) {
191+
return true;
192+
}
193+
194+
for (const auto& err : errors) {
195+
error("%s", err);
196+
}
197+
return false;
129198
}
130199

131-
bool CBanDB::Read(banmap_t& banSet)
200+
bool CBanDB::Read(banmap_t& banSet, bool& dirty)
132201
{
133-
return DeserializeFileDB(m_ban_list_path, banSet, CLIENT_VERSION);
202+
// If the JSON banlist does not exist, then try to read the non-upgraded banlist.dat.
203+
if (!fs::exists(m_banlist_json)) {
204+
// If this succeeds then we need to flush to disk in order to create the JSON banlist.
205+
dirty = true;
206+
return DeserializeFileDB(m_banlist_dat, banSet, CLIENT_VERSION);
207+
}
208+
209+
dirty = false;
210+
211+
std::map<std::string, util::SettingsValue> settings;
212+
std::vector<std::string> errors;
213+
214+
if (!util::ReadSettings(m_banlist_json, settings, errors)) {
215+
for (const auto& err : errors) {
216+
LogPrintf("Cannot load banlist %s: %s\n", m_banlist_json.string(), err);
217+
}
218+
return false;
219+
}
220+
221+
try {
222+
BanMapFromJson(settings[JSON_KEY], banSet);
223+
} catch (const std::runtime_error& e) {
224+
LogPrintf("Cannot parse banlist %s: %s\n", m_banlist_json.string(), e.what());
225+
return false;
226+
}
227+
228+
return true;
134229
}
135230

136231
CAddrDB::CAddrDB()

src/addrdb.h

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <fs.h>
1010
#include <net_types.h> // For banmap_t
1111
#include <serialize.h>
12+
#include <univalue.h>
1213

1314
#include <string>
1415
#include <vector>
@@ -36,6 +37,13 @@ class CBanEntry
3637
nCreateTime = nCreateTimeIn;
3738
}
3839

40+
/**
41+
* Create a ban entry from JSON.
42+
* @param[in] json A JSON representation of a ban entry, as created by `ToJson()`.
43+
* @throw std::runtime_error if the JSON does not have the expected fields.
44+
*/
45+
explicit CBanEntry(const UniValue& json);
46+
3947
SERIALIZE_METHODS(CBanEntry, obj)
4048
{
4149
uint8_t ban_reason = 2; //! For backward compatibility
@@ -48,6 +56,12 @@ class CBanEntry
4856
nCreateTime = 0;
4957
nBanUntil = 0;
5058
}
59+
60+
/**
61+
* Generate a JSON representation of this ban entry.
62+
* @return JSON suitable for passing to the `CBanEntry(const UniValue&)` constructor.
63+
*/
64+
UniValue ToJson() const;
5165
};
5266

5367
/** Access to the (IP) address database (peers.dat) */
@@ -62,15 +76,30 @@ class CAddrDB
6276
static bool Read(CAddrMan& addr, CDataStream& ssPeers);
6377
};
6478

65-
/** Access to the banlist database (banlist.dat) */
79+
/** Access to the banlist databases (banlist.json and banlist.dat) */
6680
class CBanDB
6781
{
6882
private:
69-
const fs::path m_ban_list_path;
83+
/**
84+
* JSON key under which the data is stored in the json database.
85+
*/
86+
static constexpr const char* JSON_KEY = "banned_nets";
87+
88+
const fs::path m_banlist_dat;
89+
const fs::path m_banlist_json;
7090
public:
7191
explicit CBanDB(fs::path ban_list_path);
7292
bool Write(const banmap_t& banSet);
73-
bool Read(banmap_t& banSet);
93+
94+
/**
95+
* Read the banlist from disk.
96+
* @param[out] banSet The loaded list. Set if `true` is returned, otherwise it is left
97+
* in an undefined state.
98+
* @param[out] dirty Indicates whether the loaded list needs flushing to disk. Set if
99+
* `true` is returned, otherwise it is left in an undefined state.
100+
* @return true on success
101+
*/
102+
bool Read(banmap_t& banSet, bool& dirty);
74103
};
75104

76105
/**

src/banman.cpp

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,18 @@ BanMan::BanMan(fs::path ban_file, CClientUIInterface* client_interface, int64_t
1818
if (m_client_interface) m_client_interface->InitMessage(_("Loading banlist…").translated);
1919

2020
int64_t n_start = GetTimeMillis();
21-
m_is_dirty = false;
22-
banmap_t banmap;
23-
if (m_ban_db.Read(banmap)) {
24-
SetBanned(banmap); // thread save setter
25-
SetBannedSetDirty(false); // no need to write down, just read data
26-
SweepBanned(); // sweep out unused entries
21+
if (m_ban_db.Read(m_banned, m_is_dirty)) {
22+
SweepBanned(); // sweep out unused entries
2723

28-
LogPrint(BCLog::NET, "Loaded %d banned node ips/subnets from banlist.dat %dms\n",
29-
m_banned.size(), GetTimeMillis() - n_start);
24+
LogPrint(BCLog::NET, "Loaded %d banned node addresses/subnets %dms\n", m_banned.size(),
25+
GetTimeMillis() - n_start);
3026
} else {
31-
LogPrintf("Recreating banlist.dat\n");
32-
SetBannedSetDirty(true); // force write
33-
DumpBanlist();
27+
LogPrintf("Recreating the banlist database\n");
28+
m_banned = {};
29+
m_is_dirty = true;
3430
}
31+
32+
DumpBanlist();
3533
}
3634

3735
BanMan::~BanMan()
@@ -53,8 +51,8 @@ void BanMan::DumpBanlist()
5351
SetBannedSetDirty(false);
5452
}
5553

56-
LogPrint(BCLog::NET, "Flushed %d banned node ips/subnets to banlist.dat %dms\n",
57-
banmap.size(), GetTimeMillis() - n_start);
54+
LogPrint(BCLog::NET, "Flushed %d banned node addresses/subnets to disk %dms\n", banmap.size(),
55+
GetTimeMillis() - n_start);
5856
}
5957

6058
void BanMan::ClearBanned()
@@ -167,13 +165,6 @@ void BanMan::GetBanned(banmap_t& banmap)
167165
banmap = m_banned; //create a thread safe copy
168166
}
169167

170-
void BanMan::SetBanned(const banmap_t& banmap)
171-
{
172-
LOCK(m_cs_banned);
173-
m_banned = banmap;
174-
m_is_dirty = true;
175-
}
176-
177168
void BanMan::SweepBanned()
178169
{
179170
int64_t now = GetTime();
@@ -188,7 +179,7 @@ void BanMan::SweepBanned()
188179
m_banned.erase(it++);
189180
m_is_dirty = true;
190181
notify_ui = true;
191-
LogPrint(BCLog::NET, "%s: Removed banned node ip/subnet from banlist.dat: %s\n", __func__, sub_net.ToString());
182+
LogPrint(BCLog::NET, "Removed banned node address/subnet: %s\n", sub_net.ToString());
192183
} else
193184
++it;
194185
}

src/banman.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717

1818
// NOTE: When adjusting this, update rpcnet:setban's help ("24h")
1919
static constexpr unsigned int DEFAULT_MISBEHAVING_BANTIME = 60 * 60 * 24; // Default 24-hour ban
20-
// How often to dump addresses to banlist.dat
20+
21+
/// How often to dump banned addresses/subnets to disk.
2122
static constexpr std::chrono::minutes DUMP_BANS_INTERVAL{15};
2223

2324
class CClientUIInterface;
@@ -30,7 +31,7 @@ class CSubNet;
3031
// If an address or subnet is banned, we never accept incoming connections from
3132
// it and never create outgoing connections to it. We won't gossip its address
3233
// 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+
// disk on shutdown and reloaded on startup. Banning can be used to
3435
// prevent connections with spy nodes or other griefers.
3536
//
3637
// 2. Discouragement. If a peer misbehaves enough (see Misbehaving() in
@@ -79,7 +80,6 @@ class BanMan
7980
void DumpBanlist();
8081

8182
private:
82-
void SetBanned(const banmap_t& banmap);
8383
bool BannedSetIsDirty();
8484
//!set the "dirty" flag for the banlist
8585
void SetBannedSetDirty(bool dirty = true);

src/init.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1161,7 +1161,7 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
11611161
assert(!node.addrman);
11621162
node.addrman = std::make_unique<CAddrMan>();
11631163
assert(!node.banman);
1164-
node.banman = std::make_unique<BanMan>(gArgs.GetDataDirNet() / "banlist.dat", &uiInterface, args.GetArg("-bantime", DEFAULT_MISBEHAVING_BANTIME));
1164+
node.banman = std::make_unique<BanMan>(gArgs.GetDataDirNet() / "banlist", &uiInterface, args.GetArg("-bantime", DEFAULT_MISBEHAVING_BANTIME));
11651165
assert(!node.connman);
11661166
node.connman = std::make_unique<CConnman>(GetRand(std::numeric_limits<uint64_t>::max()), GetRand(std::numeric_limits<uint64_t>::max()), *node.addrman, args.GetBoolArg("-networkactive", true));
11671167

src/test/denialofservice_tests.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ BOOST_AUTO_TEST_CASE(stale_tip_peer_management)
191191
BOOST_AUTO_TEST_CASE(peer_discouragement)
192192
{
193193
const CChainParams& chainparams = Params();
194-
auto banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist.dat", nullptr, DEFAULT_MISBEHAVING_BANTIME);
194+
auto banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist", nullptr, DEFAULT_MISBEHAVING_BANTIME);
195195
auto connman = std::make_unique<ConnmanTestMsg>(0x1337, 0x1337, *m_node.addrman);
196196
auto peerLogic = PeerManager::make(chainparams, *connman, *m_node.addrman, banman.get(),
197197
*m_node.scheduler, *m_node.chainman, *m_node.mempool, false);
@@ -285,7 +285,7 @@ BOOST_AUTO_TEST_CASE(peer_discouragement)
285285
BOOST_AUTO_TEST_CASE(DoS_bantime)
286286
{
287287
const CChainParams& chainparams = Params();
288-
auto banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist.dat", nullptr, DEFAULT_MISBEHAVING_BANTIME);
288+
auto banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist", nullptr, DEFAULT_MISBEHAVING_BANTIME);
289289
auto connman = std::make_unique<CConnman>(0x1337, 0x1337, *m_node.addrman);
290290
auto peerLogic = PeerManager::make(chainparams, *connman, *m_node.addrman, banman.get(),
291291
*m_node.scheduler, *m_node.chainman, *m_node.mempool, false);

src/test/fuzz/banman.cpp

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
#include <test/fuzz/fuzz.h>
1010
#include <test/fuzz/util.h>
1111
#include <test/util/setup_common.h>
12+
#include <util/readwritefile.h>
1213
#include <util/system.h>
1314

15+
#include <cassert>
1416
#include <cstdint>
1517
#include <limits>
1618
#include <string>
@@ -38,8 +40,20 @@ FUZZ_TARGET_INIT(banman, initialize_banman)
3840
int limit_max_ops{300};
3941
FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()};
4042
SetMockTime(ConsumeTime(fuzzed_data_provider));
41-
const fs::path banlist_file = gArgs.GetDataDirNet() / "fuzzed_banlist.dat";
42-
fs::remove(banlist_file);
43+
fs::path banlist_file = gArgs.GetDataDirNet() / "fuzzed_banlist";
44+
45+
const bool start_with_corrupted_banlist{fuzzed_data_provider.ConsumeBool()};
46+
if (start_with_corrupted_banlist) {
47+
const std::string sfx{fuzzed_data_provider.ConsumeBool() ? ".dat" : ".json"};
48+
assert(WriteBinaryFile(banlist_file.string() + sfx,
49+
fuzzed_data_provider.ConsumeRandomLengthString()));
50+
} else {
51+
const bool force_read_and_write_to_err{fuzzed_data_provider.ConsumeBool()};
52+
if (force_read_and_write_to_err) {
53+
banlist_file = fs::path{"path"} / "to" / "inaccessible" / "fuzzed_banlist";
54+
}
55+
}
56+
4357
{
4458
BanMan ban_man{banlist_file, nullptr, ConsumeBanTimeOffset(fuzzed_data_provider)};
4559
while (--limit_max_ops >= 0 && fuzzed_data_provider.ConsumeBool()) {
@@ -80,5 +94,6 @@ FUZZ_TARGET_INIT(banman, initialize_banman)
8094
});
8195
}
8296
}
83-
fs::remove(banlist_file);
97+
fs::remove(banlist_file.string() + ".dat");
98+
fs::remove(banlist_file.string() + ".json");
8499
}

src/test/util/setup_common.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ TestingSetup::TestingSetup(const std::string& chainName, const std::vector<const
196196
}
197197

198198
m_node.addrman = std::make_unique<CAddrMan>();
199-
m_node.banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist.dat", nullptr, DEFAULT_MISBEHAVING_BANTIME);
199+
m_node.banman = std::make_unique<BanMan>(m_args.GetDataDirBase() / "banlist", nullptr, DEFAULT_MISBEHAVING_BANTIME);
200200
m_node.connman = std::make_unique<CConnman>(0x1337, 0x1337, *m_node.addrman); // Deterministic randomness for tests.
201201
m_node.peerman = PeerManager::make(chainparams, *m_node.connman, *m_node.addrman,
202202
m_node.banman.get(), *m_node.scheduler, *m_node.chainman,

0 commit comments

Comments
 (0)