diff --git a/libtransmission/blocklist.cc b/libtransmission/blocklist.cc index b1275a57d..3d63dc92e 100644 --- a/libtransmission/blocklist.cc +++ b/libtransmission/blocklist.cc @@ -49,10 +49,63 @@ void BlocklistFile::ensureLoaded() const return; } - auto range = IPv4Range{}; - while (in.read(reinterpret_cast(&range), sizeof(range))) + auto file_info = tr_sys_path_get_info(filename_); + auto zeroes_count = 0; + auto max_zeroes = 0; + + static auto constexpr RangeSize = sizeof(AddressRange); + if (file_info->size >= RangeSize) { - rules_.emplace_back(range); + std::array first_struct = {}; + + in.read(reinterpret_cast(&first_struct), std::size(first_struct)); + in.clear(); + in.seekg(0, std::ios::beg); + + for (auto const struct_byte : first_struct) + { + if (struct_byte != 0) + { + zeroes_count = 0; + } + else + { + ++zeroes_count; + + if (zeroes_count > max_zeroes) + { + max_zeroes = zeroes_count; + } + } + } + } + + // Check for old blocklist file format + // Old struct size was 8 bytes (2 IPv4), new struct size is 40 bytes (2 IPv4, 2 IPv6) + // + // If we encounter less than 4 continuous bytes containing 0 we are using old file format + // (as the new format guarantees at least 2 empty IPv4 OR 2 empty IPv6) + // If we confirm using old style convert to new style and rewrite blocklist file + if ((file_info->size >= 40 && max_zeroes < 4) || (file_info->size % 8 == 0 && file_info->size % 40 != 0)) + { + auto range = AddressRange{}; + while (in.read(reinterpret_cast(&range), 8)) + { + rules_.emplace_back(range); + } + + tr_logAddInfo(_("Rewriting old blocklist file format to new format")); + + RewriteBlocklistFile(); + } + + else + { + auto range = AddressRange{}; + while (in.read(reinterpret_cast(&range), sizeof(range))) + { + rules_.emplace_back(range); + } } tr_logAddInfo(fmt::format( @@ -69,7 +122,7 @@ bool BlocklistFile::hasAddress(tr_address const& addr) { TR_ASSERT(tr_address_is_valid(&addr)); - if (!is_enabled_ || !addr.isIPv4()) + if (!is_enabled_) { return false; } @@ -81,22 +134,45 @@ bool BlocklistFile::hasAddress(tr_address const& addr) return false; } - auto const needle = ntohl(addr.addr.addr4.s_addr); + if (addr.isIPv4()) + { + auto const needle = ntohl(addr.addr.addr4.s_addr); - // std::binary_search works differently and requires a less-than comparison - // and two arguments of the same type. std::bsearch is the right choice. - auto const* range = static_cast( - std::bsearch(&needle, std::data(rules_), std::size(rules_), sizeof(IPv4Range), IPv4Range::compareAddressToRange)); + // std::binary_search works differently and requires a less-than comparison + // and two arguments of the same type. std::bsearch is the right choice. + auto const* range = static_cast(std::bsearch( + &needle, + std::data(rules_), + std::size(rules_), + sizeof(AddressRange), + AddressRange::compareIPv4AddressToRange)); - return range != nullptr; + return range != nullptr; + } + + if (addr.isIPv6()) + { + auto const needle = addr.addr.addr6; + + auto const* range = static_cast(std::bsearch( + &needle, + std::data(rules_), + std::size(rules_), + sizeof(AddressRange), + AddressRange::compareIPv6AddressToRange)); + + return range != nullptr; + } + + return false; } /* - * P2P plaintext format: "comment:x.x.x.x-y.y.y.y" - * http://wiki.phoenixlabs.org/wiki/P2P_Format + * P2P plaintext format: "comment:x.x.x.x-y.y.y.y" / "comment:x:x:x:x:x:x:x:x-x:x:x:x:x:x:x:x" + * https://web.archive.org/web/20100328075307/http://wiki.phoenixlabs.org/wiki/P2P_Format * https://en.wikipedia.org/wiki/PeerGuardian#P2P_plaintext_format */ -bool BlocklistFile::parseLine1(std::string_view line, struct IPv4Range* range) +bool BlocklistFile::parseLine1(std::string_view line, struct AddressRange* range) { // remove leading "comment:" auto pos = line.find(':'); @@ -114,7 +190,14 @@ bool BlocklistFile::parseLine1(std::string_view line, struct IPv4Range* range) } if (auto const addr = tr_address::fromString(line.substr(0, pos)); addr) { - range->begin_ = ntohl(addr->addr.addr4.s_addr); + if (addr->isIPv4()) + { + range->begin_ = ntohl(addr->addr.addr4.s_addr); + } + else + { + range->begin6_ = addr->addr.addr6; + } } else { @@ -125,7 +208,14 @@ bool BlocklistFile::parseLine1(std::string_view line, struct IPv4Range* range) // parse the trailing 'y.y.y.y' if (auto const addr = tr_address::fromString(line); addr) { - range->end_ = ntohl(addr->addr.addr4.s_addr); + if (addr->isIPv4()) + { + range->end_ = ntohl(addr->addr.addr4.s_addr); + } + else + { + range->end6_ = addr->addr.addr6; + } } else { @@ -139,7 +229,7 @@ bool BlocklistFile::parseLine1(std::string_view line, struct IPv4Range* range) * DAT / eMule format: "000.000.000.000 - 000.255.255.255 , 000 , invalid ip"a * https://sourceforge.net/p/peerguardian/wiki/dev-blocklist-format-dat/ */ -bool BlocklistFile::parseLine2(std::string_view line, struct IPv4Range* range) +bool BlocklistFile::parseLine2(std::string_view line, struct AddressRange* range) { static auto constexpr Delim1 = std::string_view{ " - " }; static auto constexpr Delim2 = std::string_view{ " , " }; @@ -179,10 +269,10 @@ bool BlocklistFile::parseLine2(std::string_view line, struct IPv4Range* range) } /* - * CIDR notation: "0.0.0.0/8", IPv4 only + * CIDR notation: "0.0.0.0/8", "::/64" * https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation */ -bool BlocklistFile::parseLine3(char const* line, IPv4Range* range) +bool BlocklistFile::parseLine3(char const* line, AddressRange* range) { auto ip = std::array{}; unsigned int pflen = 0; @@ -211,13 +301,19 @@ bool BlocklistFile::parseLine3(char const* line, IPv4Range* range) return true; } -bool BlocklistFile::parseLine(char const* line, IPv4Range* range) +bool BlocklistFile::parseLine(char const* line, AddressRange* range) { return parseLine1(line, range) || parseLine2(line, range) || parseLine3(line, range); } -bool BlocklistFile::compareAddressRangesByFirstAddress(IPv4Range const& a, IPv4Range const& b) +bool BlocklistFile::compareAddressRangesByFirstAddress(AddressRange const& a, AddressRange const& b) { + if (a.begin_ == 0 && a.end_ == 0) + { + // IPv6 + return (memcmp(a.begin6_.s6_addr, b.begin6_.s6_addr, sizeof(a.begin6_.s6_addr)) < 0); + } + return a.begin_ < b.begin_; } @@ -241,18 +337,17 @@ size_t BlocklistFile::setContent(char const* filename) auto line = std::string{}; auto line_number = size_t{ 0U }; - auto ranges = std::vector{}; + auto ranges = std::vector{}; while (std::getline(in, line)) { ++line_number; - auto range = IPv4Range{}; + auto range = AddressRange{}; if (!parseLine(line.c_str(), &range)) { /* don't try to display the actual lines - it causes issues */ tr_logAddWarn(fmt::format(_("Couldn't parse line: '{line}'"), fmt::arg("line", line_number))); continue; } - ranges.push_back(range); } in.close(); @@ -269,13 +364,28 @@ size_t BlocklistFile::setContent(char const* filename) // merge for (auto const& range : ranges) { - if (ranges[keep].end_ < range.begin_) + if (range.begin_ == 0 && range.end_ == 0) { - ranges[++keep] = range; + // IPv6 + if (memcmp(ranges[keep].end6_.s6_addr, range.begin6_.s6_addr, sizeof(range.begin6_.s6_addr)) < 0) + { + ranges[++keep] = range; + } + else if (memcmp(ranges[keep].end6_.s6_addr, range.end6_.s6_addr, sizeof(range.begin6_.s6_addr)) < 0) + { + ranges[keep].end6_ = range.end6_; + } } - else if (ranges[keep].end_ < range.end_) + else { - ranges[keep].end_ = range.end_; + if (ranges[keep].end_ < range.begin_) + { + ranges[++keep] = range; + } + else if (ranges[keep].end_ < range.end_) + { + ranges[keep].end_ = range.end_; + } } } @@ -297,7 +407,7 @@ size_t BlocklistFile::setContent(char const* filename) return {}; } - if (!out.write(reinterpret_cast(ranges.data()), std::size(ranges) * sizeof(IPv4Range))) + if (!out.write(reinterpret_cast(ranges.data()), std::size(ranges) * sizeof(AddressRange))) { tr_logAddWarn(fmt::format( _("Couldn't save '{path}': {error} ({error_code})"), @@ -320,17 +430,73 @@ size_t BlocklistFile::setContent(char const* filename) return std::size(rules_); } +void BlocklistFile::RewriteBlocklistFile() const +{ + auto out = std::ofstream{ filename_, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary }; + if (!out.is_open()) + { + tr_logAddWarn(fmt::format( + _("Couldn't read '{path}': {error} ({error_code})"), + fmt::arg("path", filename_), + fmt::arg("error", tr_strerror(errno)), + fmt::arg("error_code", errno))); + return; + } + + if (!out.write(reinterpret_cast(rules_.data()), std::size(rules_) * sizeof(AddressRange))) + { + tr_logAddWarn(fmt::format( + _("Couldn't save '{path}': {error} ({error_code})"), + fmt::arg("path", filename_), + fmt::arg("error", tr_strerror(errno)), + fmt::arg("error_code", errno))); + } + + out.close(); + ensureLoaded(); +} + #ifdef TR_ENABLE_ASSERTS -void BlocklistFile::assertValidRules(std::vector const& ranges) +void BlocklistFile::assertValidRules(std::vector const& ranges) { for (auto const& r : ranges) { - TR_ASSERT(r.begin_ <= r.end_); + if (r.begin_ == 0 && r.end_ == 0) + { + TR_ASSERT(memcmp(r.begin6_.s6_addr, r.end6_.s6_addr, sizeof(r.begin6_.s6_addr)) <= 0); + } + else + { + TR_ASSERT(r.begin_ <= r.end_); + } } - for (size_t i = 1; i < std::size(ranges); ++i) + auto ranges_IPv6 = std::vector{}; + auto ranges_IPv4 = std::vector{}; + + for (size_t i = 0; i < std::size(ranges); i++) { - TR_ASSERT(ranges[i - 1].end_ < ranges[i].begin_); + if (ranges[i].begin_ == 0 && ranges[i].end_ == 0) + { + ranges_IPv6.push_back(ranges[i]); + } + else + { + ranges_IPv4.push_back(ranges[i]); + } + } + + for (size_t i = 1; i < std::size(ranges_IPv4); ++i) + { + TR_ASSERT(ranges_IPv4[i - 1].end_ < ranges_IPv4[i].begin_); + } + + for (size_t i = 1; i < std::size(ranges_IPv6); ++i) + { + auto last_end_address = ranges_IPv6[i - 1].end6_.s6_addr; + auto start_address = ranges_IPv6[i].begin6_.s6_addr; + + TR_ASSERT(memcmp(last_end_address, start_address, sizeof(&start_address)) > 0); } } #endif diff --git a/libtransmission/blocklist.h b/libtransmission/blocklist.h index 582cfcec3..c3f7c9d22 100644 --- a/libtransmission/blocklist.h +++ b/libtransmission/blocklist.h @@ -15,6 +15,12 @@ #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + #include "file.h" // for tr_sys_file_t #include "tr-assert.h" #include "tr-macros.h" @@ -74,16 +80,18 @@ public: size_t setContent(char const* filename); private: - struct IPv4Range + struct AddressRange { uint32_t begin_ = 0; uint32_t end_ = 0; + in6_addr begin6_; + in6_addr end6_; /// @brief Used for std::bsearch of an IPv4 address - static int compareAddressToRange(void const* va, void const* vb) + static int compareIPv4AddressToRange(void const* va, void const* vb) { auto const* a = reinterpret_cast(va); - auto const* b = reinterpret_cast(vb); + auto const* b = reinterpret_cast(vb); if (*a < b->begin_) { @@ -97,26 +105,46 @@ private: return 0; } + + /// @brief Used for std::bsearch of an IPv6 address + static int compareIPv6AddressToRange(void const* va, void const* vb) + { + auto const* a = reinterpret_cast(va); + auto const* b = reinterpret_cast(vb); + + if (memcmp(&a->s6_addr, &b->begin6_.s6_addr, sizeof(a->s6_addr)) < 0) + { + return -1; + } + + if (memcmp(&a->s6_addr, &b->end6_.s6_addr, sizeof(a->s6_addr)) > 0) + { + return 1; + } + + return 0; + } }; + void RewriteBlocklistFile() const; void ensureLoaded() const; void load(); void close(); - static bool parseLine(char const* line, IPv4Range* range); - static bool compareAddressRangesByFirstAddress(IPv4Range const& a, IPv4Range const& b); + static bool parseLine(char const* line, AddressRange* range); + static bool compareAddressRangesByFirstAddress(AddressRange const& a, AddressRange const& b); - static bool parseLine1(std::string_view line, struct IPv4Range* range); - static bool parseLine2(std::string_view line, struct IPv4Range* range); - static bool parseLine3(char const* line, IPv4Range* range); + static bool parseLine1(std::string_view line, struct AddressRange* range); + static bool parseLine2(std::string_view line, struct AddressRange* range); + static bool parseLine3(char const* line, AddressRange* range); #ifdef TR_ENABLE_ASSERTS /// @brief Sanity checks: make sure the rules are sorted in ascending order and don't overlap - static void assertValidRules(std::vector const& ranges); + static void assertValidRules(std::vector const& ranges); #endif std::string const filename_; bool is_enabled_ = false; - mutable std::vector rules_; + mutable std::vector rules_; }; diff --git a/tests/libtransmission/blocklist-test.cc b/tests/libtransmission/blocklist-test.cc index feb6ebfd2..de5801160 100644 --- a/tests/libtransmission/blocklist-test.cc +++ b/tests/libtransmission/blocklist-test.cc @@ -31,7 +31,8 @@ protected: "Austin Law Firm:216.16.1.144-216.16.1.151\n" "Sargent Controls and Aerospace:216.19.18.0-216.19.18.255\n" "Corel Corporation:216.21.157.192-216.21.157.223\n" - "Fox Speed Channel:216.79.131.192-216.79.131.223\n"; + "Fox Speed Channel:216.79.131.192-216.79.131.223\n" + "IPv6 example:2001:db8::-2001:db8:ffff:ffff:ffff:ffff:ffff:ffff\n"; static char const constexpr* const Contents2 = "10.5.6.7/8\n" @@ -39,6 +40,7 @@ protected: "Sargent Controls and Aerospace:216.19.18.0-216.19.18.255\n" "Corel Corporation:216.21.157.192-216.21.157.223\n" "Fox Speed Channel:216.79.131.192-216.79.131.223\n" + "IPv6 example:2001:db8::-2001:db8:ffff:ffff:ffff:ffff:ffff:ffff\n" "Evilcorp:216.88.88.0-216.88.88.255\n"; #if 0 @@ -72,7 +74,7 @@ TEST_F(BlocklistTest, parsing) createFileWithContents(path, Contents1); tr_sessionReloadBlocklists(session_); EXPECT_TRUE(tr_blocklistExists(session_)); - EXPECT_EQ(size_t{ 5 }, tr_blocklistGetRuleCount(session_)); + EXPECT_EQ(size_t{ 6 }, tr_blocklistGetRuleCount(session_)); // enable the blocklist EXPECT_FALSE(tr_blocklistIsEnabled(session_)); @@ -95,6 +97,13 @@ TEST_F(BlocklistTest, parsing) EXPECT_FALSE(addressIsBlocked("216.16.1.153")); EXPECT_FALSE(addressIsBlocked("217.0.0.1")); EXPECT_FALSE(addressIsBlocked("255.0.0.1")); + // IPv6 + EXPECT_TRUE(addressIsBlocked("2001:db8:dead:beef:dead:beef:dead:beef")); + EXPECT_TRUE(addressIsBlocked("2001:db8:ffff:ffff:ffff:ffff:ffff:fffe")); + EXPECT_FALSE(addressIsBlocked("fe80:1:1:1:1:1:1:1337")); + EXPECT_FALSE(addressIsBlocked("2a05:d012:8b5:6501:b6d2:c4fb:b11:5181")); + EXPECT_FALSE(addressIsBlocked("1::1")); + EXPECT_FALSE(addressIsBlocked("ffff::ffff")); } /*** @@ -112,22 +121,22 @@ TEST_F(BlocklistTest, updating) // test that updated source files will get loaded createFileWithContents(path, Contents1); tr_sessionReloadBlocklists(session_); - EXPECT_EQ(5U, tr_blocklistGetRuleCount(session_)); + EXPECT_EQ(6U, tr_blocklistGetRuleCount(session_)); // test that updated source files will get loaded createFileWithContents(path, Contents2); tr_sessionReloadBlocklists(session_); - EXPECT_EQ(6U, tr_blocklistGetRuleCount(session_)); + EXPECT_EQ(7U, tr_blocklistGetRuleCount(session_)); // test that updated source files will get loaded createFileWithContents(path, Contents1); tr_sessionReloadBlocklists(session_); - EXPECT_EQ(5U, tr_blocklistGetRuleCount(session_)); + EXPECT_EQ(6U, tr_blocklistGetRuleCount(session_)); // ensure that new files, if bad, get skipped createFileWithContents(path, "# nothing useful\n"); tr_sessionReloadBlocklists(session_); - EXPECT_EQ(5U, tr_blocklistGetRuleCount(session_)); + EXPECT_EQ(6U, tr_blocklistGetRuleCount(session_)); // cleanup }