diff --git a/qa/rpc-tests/nodehandling.py b/qa/rpc-tests/nodehandling.py index 9a77bd97e..d89cfcf59 100755 --- a/qa/rpc-tests/nodehandling.py +++ b/qa/rpc-tests/nodehandling.py @@ -48,7 +48,25 @@ class NodeHandlingTest (BitcoinTestFramework): assert_equal(len(self.nodes[2].listbanned()), 0) self.nodes[2].clearbanned() assert_equal(len(self.nodes[2].listbanned()), 0) - + + ##test persisted banlist + self.nodes[2].setban("127.0.0.0/32", "add") + self.nodes[2].setban("127.0.0.0/24", "add") + self.nodes[2].setban("192.168.0.1", "add", 1) #ban for 1 seconds + self.nodes[2].setban("2001:4d48:ac57:400:cacf:e9ff:fe1d:9c63/19", "add", 1000) #ban for 1000 seconds + listBeforeShutdown = self.nodes[2].listbanned(); + assert_equal("192.168.0.1/255.255.255.255", listBeforeShutdown[2]['address']) #must be here + time.sleep(2) #make 100% sure we expired 192.168.0.1 node time + + #stop node + stop_node(self.nodes[2], 2) + + self.nodes[2] = start_node(2, self.options.tmpdir) + listAfterShutdown = self.nodes[2].listbanned(); + assert_equal("127.0.0.0/255.255.255.0", listAfterShutdown[0]['address']) + assert_equal("127.0.0.0/255.255.255.255", listAfterShutdown[1]['address']) + assert_equal("2001:4000::/ffff:e000:0:0:0:0:0:0", listAfterShutdown[2]['address']) + ########################### # RPC disconnectnode test # ########################### diff --git a/src/net.cpp b/src/net.cpp index 0511256e5..3a549d65c 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -445,11 +445,13 @@ void CNode::PushVersion() std::map CNode::setBanned; CCriticalSection CNode::cs_setBanned; +bool CNode::setBannedIsDirty; void CNode::ClearBanned() { LOCK(cs_setBanned); setBanned.clear(); + setBannedIsDirty = true; } bool CNode::IsBanned(CNetAddr ip) @@ -498,6 +500,8 @@ void CNode::Ban(const CSubNet& subNet, int64_t bantimeoffset, bool sinceUnixEpoc LOCK(cs_setBanned); if (setBanned[subNet] < banTime) setBanned[subNet] = banTime; + + setBannedIsDirty = true; } bool CNode::Unban(const CNetAddr &addr) { @@ -508,7 +512,10 @@ bool CNode::Unban(const CNetAddr &addr) { bool CNode::Unban(const CSubNet &subNet) { LOCK(cs_setBanned); if (setBanned.erase(subNet)) + { + setBannedIsDirty = true; return true; + } return false; } @@ -518,6 +525,43 @@ void CNode::GetBanned(std::map &banMap) banMap = setBanned; //create a thread safe copy } +void CNode::SetBanned(const std::map &banMap) +{ + LOCK(cs_setBanned); + setBanned = banMap; + setBannedIsDirty = true; +} + +void CNode::SweepBanned() +{ + int64_t now = GetTime(); + + LOCK(cs_setBanned); + std::map::iterator it = setBanned.begin(); + while(it != setBanned.end()) + { + if(now > (*it).second) + { + setBanned.erase(it++); + setBannedIsDirty = true; + } + else + ++it; + } +} + +bool CNode::BannedSetIsDirty() +{ + LOCK(cs_setBanned); + return setBannedIsDirty; +} + +void CNode::SetBannedSetDirty(bool dirty) +{ + LOCK(cs_setBanned); //reuse setBanned lock for the isDirty flag + setBannedIsDirty = dirty; +} + std::vector CNode::vWhitelistedRange; CCriticalSection CNode::cs_vWhitelistedRange; @@ -1212,6 +1256,17 @@ void DumpAddresses() addrman.size(), GetTimeMillis() - nStart); } +void DumpData() +{ + DumpAddresses(); + + if (CNode::BannedSetIsDirty()) + { + DumpBanlist(); + CNode::SetBannedSetDirty(false); + } +} + void static ProcessOneShot() { string strDest; @@ -1650,6 +1705,17 @@ void StartNode(boost::thread_group& threadGroup, CScheduler& scheduler) if (!adb.Read(addrman)) LogPrintf("Invalid or missing peers.dat; recreating\n"); } + + //try to read stored banlist + CBanDB bandb; + std::map banmap; + if (!bandb.Read(banmap)) + LogPrintf("Invalid or missing banlist.dat; recreating\n"); + + CNode::SetBanned(banmap); //thread save setter + CNode::SetBannedSetDirty(false); //no need to write down just read or nonexistent data + CNode::SweepBanned(); //sweap out unused entries + LogPrintf("Loaded %i addresses from peers.dat %dms\n", addrman.size(), GetTimeMillis() - nStart); fAddressesInitialized = true; @@ -1690,7 +1756,7 @@ void StartNode(boost::thread_group& threadGroup, CScheduler& scheduler) threadGroup.create_thread(boost::bind(&TraceThread, "msghand", &ThreadMessageHandler)); // Dump network addresses - scheduler.scheduleEvery(&DumpAddresses, DUMP_ADDRESSES_INTERVAL); + scheduler.scheduleEvery(&DumpData, DUMP_ADDRESSES_INTERVAL); } bool StopNode() @@ -1703,7 +1769,7 @@ bool StopNode() if (fAddressesInitialized) { - DumpAddresses(); + DumpData(); fAddressesInitialized = false; } @@ -2107,3 +2173,119 @@ void CNode::EndMessage() UNLOCK_FUNCTION(cs_vSend) LEAVE_CRITICAL_SECTION(cs_vSend); } + +// +// CBanDB +// + +CBanDB::CBanDB() +{ + pathBanlist = GetDataDir() / "banlist.dat"; +} + +bool CBanDB::Write(const std::map& banSet) +{ + // Generate random temporary filename + unsigned short randv = 0; + GetRandBytes((unsigned char*)&randv, sizeof(randv)); + std::string tmpfn = strprintf("banlist.dat.%04x", randv); + + // serialize banlist, checksum data up to that point, then append csum + CDataStream ssBanlist(SER_DISK, CLIENT_VERSION); + ssBanlist << FLATDATA(Params().MessageStart()); + ssBanlist << banSet; + uint256 hash = Hash(ssBanlist.begin(), ssBanlist.end()); + ssBanlist << hash; + + // open temp output file, and associate with CAutoFile + boost::filesystem::path pathTmp = GetDataDir() / tmpfn; + FILE *file = fopen(pathTmp.string().c_str(), "wb"); + CAutoFile fileout(file, SER_DISK, CLIENT_VERSION); + if (fileout.IsNull()) + return error("%s: Failed to open file %s", __func__, pathTmp.string()); + + // Write and commit header, data + try { + fileout << ssBanlist; + } + catch (const std::exception& e) { + return error("%s: Serialize or I/O error - %s", __func__, e.what()); + } + FileCommit(fileout.Get()); + fileout.fclose(); + + // replace existing banlist.dat, if any, with new banlist.dat.XXXX + if (!RenameOver(pathTmp, pathBanlist)) + return error("%s: Rename-into-place failed", __func__); + + return true; +} + +bool CBanDB::Read(std::map& banSet) +{ + // open input file, and associate with CAutoFile + FILE *file = fopen(pathBanlist.string().c_str(), "rb"); + CAutoFile filein(file, SER_DISK, CLIENT_VERSION); + if (filein.IsNull()) + return error("%s: Failed to open file %s", __func__, pathBanlist.string()); + + // use file size to size memory buffer + int fileSize = boost::filesystem::file_size(pathBanlist); + int dataSize = fileSize - sizeof(uint256); + // Don't try to resize to a negative number if file is small + if (dataSize < 0) + dataSize = 0; + vector vchData; + vchData.resize(dataSize); + uint256 hashIn; + + // read data and checksum from file + try { + filein.read((char *)&vchData[0], dataSize); + filein >> hashIn; + } + catch (const std::exception& e) { + return error("%s: Deserialize or I/O error - %s", __func__, e.what()); + } + filein.fclose(); + + CDataStream ssBanlist(vchData, SER_DISK, CLIENT_VERSION); + + // verify stored checksum matches input data + uint256 hashTmp = Hash(ssBanlist.begin(), ssBanlist.end()); + if (hashIn != hashTmp) + return error("%s: Checksum mismatch, data corrupted", __func__); + + unsigned char pchMsgTmp[4]; + try { + // de-serialize file header (network specific magic number) and .. + ssBanlist >> FLATDATA(pchMsgTmp); + + // ... verify the network matches ours + if (memcmp(pchMsgTmp, Params().MessageStart(), sizeof(pchMsgTmp))) + return error("%s: Invalid network magic number", __func__); + + // de-serialize address data into one CAddrMan object + ssBanlist >> banSet; + } + catch (const std::exception& e) { + return error("%s: Deserialize or I/O error - %s", __func__, e.what()); + } + + return true; +} + +void DumpBanlist() +{ + int64_t nStart = GetTimeMillis(); + + CNode::SweepBanned(); //clean unused entires (if bantime has expired) + + CBanDB bandb; + std::map banmap; + CNode::GetBanned(banmap); + bandb.Write(banmap); + + LogPrint("net", "Flushed %d banned node ips/subnets to banlist.dat %dms\n", + banmap.size(), GetTimeMillis() - nStart); +} \ No newline at end of file diff --git a/src/net.h b/src/net.h index 69e4c592a..42c859e46 100644 --- a/src/net.h +++ b/src/net.h @@ -287,6 +287,7 @@ protected: // Key is IP address, value is banned-until-time static std::map setBanned; static CCriticalSection cs_setBanned; + static bool setBannedIsDirty; // Whitelisted ranges. Any node connecting from these is automatically // whitelisted (as well as those connecting to whitelisted binds). @@ -613,6 +614,14 @@ public: static bool Unban(const CNetAddr &ip); static bool Unban(const CSubNet &ip); static void GetBanned(std::map &banmap); + static void SetBanned(const std::map &banmap); + + //!check is the banlist has unwritten changes + static bool BannedSetIsDirty(); + //!set the "dirty" flag for the banlist + static void SetBannedSetDirty(bool dirty=true); + //!clean unused entires (if bantime has expired) + static void SweepBanned(); void copyStats(CNodeStats &stats); @@ -644,4 +653,17 @@ public: bool Read(CAddrMan& addr); }; +/** Access to the banlist database (banlist.dat) */ +class CBanDB +{ +private: + boost::filesystem::path pathBanlist; +public: + CBanDB(); + bool Write(const std::map& banSet); + bool Read(std::map& banSet); +}; + +void DumpBanlist(); + #endif // BITCOIN_NET_H diff --git a/src/netbase.h b/src/netbase.h index 27f0eac2a..48c2d3455 100644 --- a/src/netbase.h +++ b/src/netbase.h @@ -126,6 +126,15 @@ class CSubNet friend bool operator==(const CSubNet& a, const CSubNet& b); friend bool operator!=(const CSubNet& a, const CSubNet& b); friend bool operator<(const CSubNet& a, const CSubNet& b); + + ADD_SERIALIZE_METHODS; + + template + inline void SerializationOp(Stream& s, Operation ser_action, int nType, int nVersion) { + READWRITE(network); + READWRITE(FLATDATA(netmask)); + READWRITE(FLATDATA(valid)); + } }; /** A combination of a network address (CNetAddr) and a (TCP) port */ diff --git a/src/rpcnet.cpp b/src/rpcnet.cpp index 1572b1668..0c3745a7d 100644 --- a/src/rpcnet.cpp +++ b/src/rpcnet.cpp @@ -527,6 +527,7 @@ UniValue setban(const UniValue& params, bool fHelp) throw JSONRPCError(RPC_MISC_ERROR, "Error: Unban failed"); } + DumpBanlist(); //store banlist to disk return NullUniValue; } @@ -568,6 +569,7 @@ UniValue clearbanned(const UniValue& params, bool fHelp) ); CNode::ClearBanned(); + DumpBanlist(); //store banlist to disk return NullUniValue; }