Add hostname resolving to dosguard (#1000)

Fixes #983.

Cassandra, ETL sorces and cache already support hostname resolving.

Also added config to show missing includes by clangd.
This commit is contained in:
Sergey Kuznetsov
2023-11-29 15:13:40 +00:00
committed by GitHub
parent 35f119a268
commit 24c562fa2a
7 changed files with 216 additions and 31 deletions

View File

@@ -126,6 +126,7 @@ CheckOptions:
readability-braces-around-statements.ShortStatementLines: 2
bugprone-unsafe-functions.ReportMoreUnsafeFunctions: true
bugprone-unused-return-value.CheckedReturnTypes: ::std::error_code;::std::error_condition;::std::errc;::std::expected
misc-include-cleaner.IgnoreHeaders: ".*/(detail|impl)/.*"
HeaderFilterRegex: '^.*/(src|unitests)/.*\.(h|hpp)$'
WarningsAsErrors: '*'

5
.clangd Normal file
View File

@@ -0,0 +1,5 @@
Diagnostics:
UnusedIncludes: Strict
MissingIncludes: Strict
Includes:
IgnoreHeader: ".*/(detail|impl)/.*"

View File

@@ -102,6 +102,7 @@ target_sources (clio PRIVATE
## Web
src/web/impl/AdminVerificationStrategy.cpp
src/web/IntervalSweepHandler.cpp
src/web/Resolver.cpp
## RPC
src/rpc/Errors.cpp
src/rpc/Factories.cpp

69
src/web/Resolver.cpp Normal file
View File

@@ -0,0 +1,69 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include "web/Resolver.h"
#include <boost/asio/ip/address.hpp>
#include <string>
#include <string_view>
#include <vector>
namespace asio = boost::asio;
namespace web {
namespace {
// Check if the hostname is an IP address or a subnet.
bool
isAddress(std::string_view hostname)
{
boost::system::error_code ec;
asio::ip::make_address(hostname.data(), ec);
if (ec == boost::system::errc::success) {
return true;
}
asio::ip::make_network_v4(hostname.data(), ec);
if (ec == boost::system::errc::success) {
return true;
}
asio::ip::make_network_v6(hostname.data(), ec);
return ec == boost::system::errc::success;
}
} // namespace
std::vector<std::string>
Resolver::resolve(std::string_view hostname, std::string_view service)
{
if (isAddress(hostname)) {
return {std::string(hostname)};
}
std::vector<std::string> endpoints;
for (auto const& endpoint : resolver_.resolve(hostname, service)) {
endpoints.push_back(endpoint.endpoint().address().to_string());
}
return endpoints;
}
} // namespace web

60
src/web/Resolver.h Normal file
View File

@@ -0,0 +1,60 @@
//------------------------------------------------------------------------------
/*
This file is part of clio: https://github.com/XRPLF/clio
Copyright (c) 2023, the clio developers.
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#pragma once
#include <boost/asio.hpp>
#include <concepts>
#include <string>
#include <vector>
namespace web {
template <typename T>
concept SomeResolver = requires(T t) {
std::is_default_constructible_v<T>;
{
t.resolve(std::string_view{}, std::string_view{})
} -> std::same_as<std::vector<std::string>>;
};
/**
* @brief Simple hostnames to IP addresses resolver.
*/
class Resolver {
public:
/**
* @brief Resolve hostname to IP addresses.
*
* @throw This method throws an exception when the hostname cannot be resolved.
*
* @param hostname Hostname to resolve
* @param service Service to resolve (could be empty or port number or http)
* @return std::vector<std::string> IP addresses of the hostname
*/
std::vector<std::string>
resolve(std::string_view hostname, std::string_view service = "");
private:
boost::asio::io_context ioContext_;
boost::asio::ip::tcp::resolver resolver_{ioContext_};
};
} // namespace web

View File

@@ -20,6 +20,7 @@
#pragma once
#include "util/config/Config.h"
#include "web/Resolver.h"
#include <boost/asio.hpp>
#include <boost/iterator/transform_iterator.hpp>
@@ -144,9 +145,10 @@ public:
*
* @param config The Clio config to use
*/
WhitelistHandler(util::Config const& config)
template <SomeResolver HostnameResolverType = Resolver>
WhitelistHandler(util::Config const& config, HostnameResolverType&& resolver = {})
{
std::unordered_set<std::string> const arr = getWhitelist(config);
std::unordered_set<std::string> const arr = getWhitelist(config, std::forward<HostnameResolverType>(resolver));
for (auto const& net : arr)
whitelist_.add(net);
}
@@ -161,18 +163,28 @@ public:
}
private:
template <SomeResolver HostnameResolverType>
[[nodiscard]] static std::unordered_set<std::string>
getWhitelist(util::Config const& config)
getWhitelist(util::Config const& config, HostnameResolverType&& resolver)
{
using SetType = std::unordered_set<std::string> const;
auto whitelist = config.arrayOr("dos_guard.whitelist", {});
auto const transform = [](auto const& elem) { return elem.template value<std::string>(); };
return SetType{
std::unordered_set<std::string> const hostnames{
boost::transform_iterator(std::begin(whitelist), transform),
boost::transform_iterator(std::end(whitelist), transform)
};
// resolve hostnames to ips
std::unordered_set<std::string> ips;
for (auto const& hostname : hostnames) {
auto resolvedIps = resolver.resolve(hostname, "");
for (auto& ip : resolvedIps) {
ips.insert(std::move(ip));
}
};
return ips;
}
};
} // namespace web

View File

@@ -21,40 +21,44 @@
#include "web/WhitelistHandler.h"
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <string>
#include <string_view>
#include <vector>
using namespace util;
using namespace web;
constexpr static auto JSONDataIPV4 = R"JSON(
{
"dos_guard": {
"whitelist": [
"127.0.0.1",
"192.168.0.1/22",
"10.0.0.1"
]
}
}
)JSON";
constexpr static auto JSONDataIPV6 = R"JSON(
{
"dos_guard": {
"whitelist": [
"2002:1dd8:85a7:0000:0000:8a6e:0000:1111",
"2001:0db8:85a3:0000:0000:8a2e:0000:0000/22"
]
}
}
)JSON";
class WhitelistHandlerTest : public NoLoggerFixture {};
struct WhitelistHandlerTest : NoLoggerFixture {};
TEST_F(WhitelistHandlerTest, TestWhiteListIPV4)
{
struct MockResolver {
MOCK_METHOD(std::vector<std::string>, resolve, (std::string_view, std::string_view));
};
testing::StrictMock<MockResolver> mockResolver;
constexpr static auto JSONDataIPV4 = R"JSON(
{
"dos_guard": {
"whitelist": [
"127.0.0.1",
"192.168.0.1/22",
"10.0.0.1"
]
}
}
)JSON";
EXPECT_CALL(mockResolver, resolve(testing::_, ""))
.Times(3)
.WillRepeatedly([](auto hostname, auto) -> std::vector<std::string> { return {std::string{hostname}}; });
Config const cfg{boost::json::parse(JSONDataIPV4)};
WhitelistHandler const whitelistHandler{cfg};
WhitelistHandler const whitelistHandler{cfg, mockResolver};
EXPECT_TRUE(whitelistHandler.isWhiteListed("192.168.1.10"));
EXPECT_FALSE(whitelistHandler.isWhiteListed("193.168.0.123"));
@@ -62,8 +66,41 @@ TEST_F(WhitelistHandlerTest, TestWhiteListIPV4)
EXPECT_FALSE(whitelistHandler.isWhiteListed("10.0.0.2"));
}
TEST_F(WhitelistHandlerTest, TestWhiteListResolvesHostname)
{
constexpr static auto JSONDataIPV4 = R"JSON(
{
"dos_guard": {
"whitelist": [
"localhost",
"10.0.0.1"
]
}
}
)JSON";
Config const cfg{boost::json::parse(JSONDataIPV4)};
WhitelistHandler const whitelistHandler{cfg};
EXPECT_TRUE(whitelistHandler.isWhiteListed("127.0.0.1"));
EXPECT_FALSE(whitelistHandler.isWhiteListed("193.168.0.123"));
EXPECT_TRUE(whitelistHandler.isWhiteListed("10.0.0.1"));
EXPECT_FALSE(whitelistHandler.isWhiteListed("10.0.0.2"));
}
TEST_F(WhitelistHandlerTest, TestWhiteListIPV6)
{
constexpr static auto JSONDataIPV6 = R"JSON(
{
"dos_guard": {
"whitelist": [
"2002:1dd8:85a7:0000:0000:8a6e:0000:1111",
"2001:0db8:85a3:0000:0000:8a2e:0000:0000/22"
]
}
}
)JSON";
Config const cfg{boost::json::parse(JSONDataIPV6)};
WhitelistHandler const whitelistHandler{cfg};