feat: Add IPv6 blocklist support (#3835)
This commit is contained in:
parent
f27e1b52f4
commit
1cd6c0464c
|
@ -49,10 +49,63 @@ void BlocklistFile::ensureLoaded() const
|
|||
return;
|
||||
}
|
||||
|
||||
auto range = IPv4Range{};
|
||||
while (in.read(reinterpret_cast<char*>(&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<char, 40> first_struct = {};
|
||||
|
||||
in.read(reinterpret_cast<char*>(&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<char*>(&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<char*>(&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<IPv4Range const*>(
|
||||
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<AddressRange const*>(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<AddressRange const*>(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, 4>{};
|
||||
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<IPv4Range>{};
|
||||
auto ranges = std::vector<AddressRange>{};
|
||||
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<char const*>(ranges.data()), std::size(ranges) * sizeof(IPv4Range)))
|
||||
if (!out.write(reinterpret_cast<char const*>(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<char const*>(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<IPv4Range> const& ranges)
|
||||
void BlocklistFile::assertValidRules(std::vector<AddressRange> 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<AddressRange>{};
|
||||
auto ranges_IPv4 = std::vector<AddressRange>{};
|
||||
|
||||
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
|
||||
|
|
|
@ -15,6 +15,12 @@
|
|||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <netinet/in.h>
|
||||
#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<uint32_t const*>(va);
|
||||
auto const* b = reinterpret_cast<IPv4Range const*>(vb);
|
||||
auto const* b = reinterpret_cast<AddressRange const*>(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<in6_addr const*>(va);
|
||||
auto const* b = reinterpret_cast<AddressRange const*>(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<IPv4Range> const& ranges);
|
||||
static void assertValidRules(std::vector<AddressRange> const& ranges);
|
||||
#endif
|
||||
|
||||
std::string const filename_;
|
||||
|
||||
bool is_enabled_ = false;
|
||||
mutable std::vector<IPv4Range> rules_;
|
||||
mutable std::vector<AddressRange> rules_;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue