feat: match IPv4-mapped addresses with IPv4 whitelist (#7523)

* feat: match IPv4-mapped addresses with IPv4 whitelist

* test: for converting IPv4-mapped to native IPv4

* perf: avoid copying in `tr_wildmat()`
This commit is contained in:
Yat Ho
2025-11-08 10:26:45 +08:00
committed by GitHub
parent 312078806d
commit b3424ed260
6 changed files with 75 additions and 14 deletions

View File

@@ -798,6 +798,16 @@ int tr_address::compare(tr_address const& that) const noexcept // <=>
return false;
}
std::optional<tr_address> tr_address::from_ipv4_mapped() const noexcept
{
if (!is_ipv4_mapped_address())
{
return {};
}
return from_compact_ipv4(reinterpret_cast<std::byte const*>(&addr.addr6.s6_addr) + 12).first;
}
// --- tr_socket_addrses
std::string tr_socket_address::display_name(tr_address const& address, tr_port port) noexcept

View File

@@ -257,6 +257,8 @@ struct tr_address
return is_ipv6() && IN6_IS_ADDR_LINKLOCAL(&addr.addr6);
}
[[nodiscard]] std::optional<tr_address> from_ipv4_mapped() const noexcept;
tr_address_type type = NUM_TR_AF_INET_TYPES;
union
{

View File

@@ -389,8 +389,20 @@ bool is_address_allowed(tr_rpc_server const* server, char const* address)
return true;
}
// Convert IPv4-mapped address to IPv4 address
// so that it can match with IPv4 whitelist entries
auto native = std::string{};
if (auto ipv4_mapped = tr_address::from_string(address); ipv4_mapped)
{
if (auto addr = ipv4_mapped->from_ipv4_mapped(); addr)
{
native = addr->display_name();
}
}
auto const* const addr = std::empty(native) ? address : native.c_str();
auto const& src = server->whitelist_;
return std::any_of(std::begin(src), std::end(src), [&address](auto const& s) { return tr_wildmat(address, s); });
return std::any_of(std::begin(src), std::end(src), [&addr](auto const& s) { return tr_wildmat(addr, s.c_str()); });
}
bool isIPAddressWithOptionalPort(char const* host)
@@ -440,7 +452,11 @@ bool isHostnameAllowed(tr_rpc_server const* server, evhttp_request* const req)
}
auto const& src = server->host_whitelist_;
return std::any_of(std::begin(src), std::end(src), [&hostname](auto const& str) { return tr_wildmat(hostname, str); });
auto const hostname_sz = tr_urlbuf{ hostname };
return std::any_of(
std::begin(src),
std::end(src),
[&hostname_sz](auto const& str) { return tr_wildmat(hostname_sz, str.c_str()); });
}
bool test_session_id(tr_rpc_server const* server, evhttp_request* const req)

View File

@@ -245,12 +245,12 @@ size_t tr_strv_to_buf(std::string_view src, char* buf, size_t buflen)
return len;
}
/* User-level routine. returns whether or not 'text' and 'p' matched */
bool tr_wildmat(std::string_view text, std::string_view pattern)
/* User-level routine. returns whether or not 'text' and 'pattern' matched */
bool tr_wildmat(char const* text, char const* pattern)
{
// TODO(ckerr): replace wildmat with base/strings/pattern.cc
// wildmat wants these to be zero-terminated.
return pattern == "*"sv || DoMatch(std::string{ text }.c_str(), std::string{ pattern }.c_str()) > 0;
return (pattern[0] == '*' && pattern[1] == '\0') || DoMatch(text, pattern) > 0;
}
char const* tr_strerror(int errnum)

View File

@@ -112,7 +112,7 @@ template<typename T>
* @brief Rich Salz's classic implementation of shell-style pattern matching for `?`, `\`, `[]`, and `*` characters.
* @return 1 if the pattern matches, 0 if it doesn't, or -1 if an error occurred
*/
[[nodiscard]] bool tr_wildmat(std::string_view text, std::string_view pattern);
[[nodiscard]] bool tr_wildmat(char const* text, char const* pattern);
template<typename T>
[[nodiscard]] constexpr bool tr_strv_contains(std::string_view sv, T key) noexcept // c++23

View File

@@ -213,14 +213,16 @@ TEST_F(NetTest, isGlobalUnicastAddress)
TEST_F(NetTest, ipCompare)
{
static constexpr auto IpPairs = std::array{ std::tuple{ "223.18.245.229"sv, "8.8.8.8"sv, 1 },
std::tuple{ "0.0.0.0"sv, "255.255.255.255"sv, -1 },
std::tuple{ "8.8.8.8"sv, "8.8.8.8"sv, 0 },
std::tuple{ "8.8.8.8"sv, "2001:0:0eab:dead::a0:abcd:4e"sv, -1 },
std::tuple{ "2001:1890:1112:1::20"sv, "2001:0:0eab:dead::a0:abcd:4e"sv, 1 },
std::tuple{ "2001:1890:1112:1::20"sv, "[2001:0:0eab:dead::a0:abcd:4e]"sv, 1 },
std::tuple{ "2001:1890:1112:1::20"sv, "2001:1890:1112:1::20"sv, 0 },
std::tuple{ "2001:1890:1112:1::20"sv, "[2001:1890:1112:1::20]"sv, 0 } };
static constexpr auto IpPairs = std::array{
std::tuple{ "223.18.245.229"sv, "8.8.8.8"sv, 1 },
std::tuple{ "0.0.0.0"sv, "255.255.255.255"sv, -1 },
std::tuple{ "8.8.8.8"sv, "8.8.8.8"sv, 0 },
std::tuple{ "8.8.8.8"sv, "2001:0:0eab:dead::a0:abcd:4e"sv, -1 },
std::tuple{ "2001:1890:1112:1::20"sv, "2001:0:0eab:dead::a0:abcd:4e"sv, 1 },
std::tuple{ "2001:1890:1112:1::20"sv, "[2001:0:0eab:dead::a0:abcd:4e]"sv, 1 },
std::tuple{ "2001:1890:1112:1::20"sv, "2001:1890:1112:1::20"sv, 0 },
std::tuple{ "2001:1890:1112:1::20"sv, "[2001:1890:1112:1::20]"sv, 0 },
};
for (auto const& [sv1, sv2, res] : IpPairs)
{
@@ -235,3 +237,34 @@ TEST_F(NetTest, ipCompare)
EXPECT_EQ(ip1 == ip2, res == 0) << sv1 << ' ' << sv2;
}
}
TEST_F(NetTest, IPv4MappedAddress)
{
static auto constexpr Tests = std::array<std::pair<std::string_view, std::string_view>, 14>{ {
{ "::ffff:1.0.0.0"sv, "1.0.0.0"sv },
{ "::ffff:10.0.0.0"sv, "10.0.0.0"sv },
{ "::ffff:10.255.0.0"sv, "10.255.0.0"sv },
{ "::ffff:10.255.0.255"sv, "10.255.0.255"sv },
{ "::ffff:100.64.0.0"sv, "100.64.0.0"sv },
{ "::ffff:100.128.0.0"sv, "100.128.0.0"sv },
{ "::ffff:126.0.0.0"sv, "126.0.0.0"sv },
{ "::ffff:127.0.0.0"sv, "127.0.0.0"sv },
{ "::ffff:169.253.255.255"sv, "169.253.255.255"sv },
{ "::ffff:169.254.0.0"sv, "169.254.0.0"sv },
{ "::ffff:169.254.255.255"sv, "169.254.255.255"sv },
{ "::ffff:169.255.0.0"sv, "169.255.0.0"sv },
{ "::ffff:223.0.0.0"sv, "223.0.0.0"sv },
{ "::ffff:224.0.0.0"sv, "224.0.0.0"sv },
} };
for (auto const& [mapped_sv, native_sv] : Tests)
{
auto const mapped = tr_address::from_string(mapped_sv);
ASSERT_TRUE(mapped);
auto const native = mapped->from_ipv4_mapped();
ASSERT_TRUE(native);
EXPECT_EQ(native_sv, native->display_name());
}
}