Support whitelisting for IPV4/IPV6 with CIDR (#796)

Fixes #244
This commit is contained in:
Peter Chen
2023-08-08 11:04:16 -04:00
committed by GitHub
parent 5411fd7497
commit fc1b5ae4da
17 changed files with 364 additions and 68 deletions

View File

@@ -201,7 +201,9 @@ if(tests)
unittests/backend/cassandra/AsyncExecutorTests.cpp
# Webserver
unittests/webserver/ServerTest.cpp
unittests/webserver/RPCServerHandlerTest.cpp)
unittests/webserver/RPCServerHandlerTest.cpp
unittests/webserver/WhitelistHandlerTest.cpp
unittests/webserver/SweepHandlerTest.cpp)
include(CMake/deps/gtest.cmake)

View File

@@ -179,7 +179,8 @@ try
// Rate limiter, to prevent abuse
auto sweepHandler = IntervalSweepHandler{config, ioc};
auto dosGuard = DOSGuard{config, sweepHandler};
auto whitelistHandler = WhitelistHandler{config};
auto dosGuard = DOSGuard{config, whitelistHandler, sweepHandler};
// Interface to the database
auto backend = Backend::make_Backend(ioc, config);

View File

@@ -19,15 +19,16 @@
#pragma once
#include <config/Config.h>
#include <webserver/impl/WhitelistHandler.h>
#include <boost/asio.hpp>
#include <boost/iterator/transform_iterator.hpp>
#include <boost/system/error_code.hpp>
#include <algorithm>
#include <chrono>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <config/Config.h>
#include <ctime>
namespace clio {
@@ -43,9 +44,10 @@ public:
/**
* @brief A simple denial of service guard used for rate limiting.
*
* @tparam SweepHandler Type of the sweep handler
* @tparam Type of the Whitelist Handler
* @tparam Type of the Sweep Handler
*/
template <typename SweepHandler>
template <typename WhitelistHandlerType, typename SweepHandlerType>
class BasicDOSGuard : public BaseDOSGuard
{
// Accumulated state per IP, state will be reset accordingly
@@ -61,7 +63,7 @@ class BasicDOSGuard : public BaseDOSGuard
// accumulated states map
std::unordered_map<std::string, ClientState> ipState_;
std::unordered_map<std::string, std::uint32_t> ipConnCount_;
std::unordered_set<std::string> const whitelist_;
std::reference_wrapper<WhitelistHandlerType const> whitelistHandler_;
std::uint32_t const maxFetches_;
std::uint32_t const maxConnCount_;
@@ -73,10 +75,14 @@ public:
* @brief Constructs a new DOS guard.
*
* @param config Clio config
* @param sweepHandler Sweep handler that implements the sweeping behaviour
* @param WhitelistHandlerType Whitelist handler that checks whitelist for ip addresses
* @param SweepHandlerType Sweep handler that implements the sweeping behaviour
*/
BasicDOSGuard(clio::Config const& config, SweepHandler& sweepHandler)
: whitelist_{getWhitelist(config)}
BasicDOSGuard(
clio::Config const& config,
WhitelistHandlerType const& whitelistHandler,
SweepHandlerType& sweepHandler)
: whitelistHandler_{std::cref(whitelistHandler)}
, maxFetches_{config.valueOr("dos_guard.max_fetches", 1000000u)}
, maxConnCount_{config.valueOr("dos_guard.max_connections", 20u)}
, maxRequestCount_{config.valueOr("dos_guard.max_requests", 20u)}
@@ -92,9 +98,9 @@ public:
* @return false
*/
[[nodiscard]] bool
isWhiteListed(std::string const& ip) const noexcept
isWhiteListed(std::string_view const ip) const noexcept
{
return whitelist_.contains(ip);
return whitelistHandler_.get().isWhiteListed(ip);
}
/**
@@ -107,7 +113,7 @@ public:
[[nodiscard]] bool
isOk(std::string const& ip) const noexcept
{
if (whitelist_.contains(ip))
if (whitelistHandler_.get().isWhiteListed(ip))
return true;
{
@@ -144,7 +150,7 @@ public:
void
increment(std::string const& ip) noexcept
{
if (whitelist_.contains(ip))
if (whitelistHandler_.get().isWhiteListed(ip))
return;
std::scoped_lock lck{mtx_};
ipConnCount_[ip]++;
@@ -158,7 +164,7 @@ public:
void
decrement(std::string const& ip) noexcept
{
if (whitelist_.contains(ip))
if (whitelistHandler_.get().isWhiteListed(ip))
return;
std::scoped_lock lck{mtx_};
assert(ipConnCount_[ip] > 0);
@@ -182,7 +188,7 @@ public:
[[maybe_unused]] bool
add(std::string const& ip, uint32_t numObjects) noexcept
{
if (whitelist_.contains(ip))
if (whitelistHandler_.get().isWhiteListed(ip))
return true;
{
@@ -207,7 +213,7 @@ public:
[[maybe_unused]] bool
request(std::string const& ip) noexcept
{
if (whitelist_.contains(ip))
if (whitelistHandler_.get().isWhiteListed(ip))
return true;
{
@@ -303,6 +309,6 @@ private:
}
};
using DOSGuard = BasicDOSGuard<IntervalSweepHandler>;
using DOSGuard = BasicDOSGuard<WhitelistHandler, IntervalSweepHandler>;
} // namespace clio

View File

@@ -20,7 +20,7 @@
#pragma once
#include <webserver/PlainWsSession.h>
#include <webserver/details/HttpBase.h>
#include <webserver/impl/HttpBase.h>
namespace Server {

View File

@@ -19,7 +19,7 @@
#pragma once
#include <webserver/details/WsBase.h>
#include <webserver/impl/WsBase.h>
namespace Server {

View File

@@ -25,7 +25,7 @@
#include <rpc/common/impl/APIVersionParser.h>
#include <util/JsonUtils.h>
#include <util/Profiler.h>
#include <webserver/details/ErrorHandling.h>
#include <webserver/impl/ErrorHandling.h>
#include <boost/json/parse.hpp>

View File

@@ -20,7 +20,7 @@
#pragma once
#include <webserver/SslWsSession.h>
#include <webserver/details/HttpBase.h>
#include <webserver/impl/HttpBase.h>
namespace Server {

View File

@@ -19,7 +19,7 @@
#pragma once
#include <webserver/details/WsBase.h>
#include <webserver/impl/WsBase.h>
namespace Server {

View File

@@ -0,0 +1,167 @@
//------------------------------------------------------------------------------
/*
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 <fmt/core.h>
#include <regex>
#include <string>
#include <unordered_map>
#include <unordered_set>
namespace clio {
/**
* @brief A whitelist to remove rate limits of certain IP addresses
*
*/
class Whitelist
{
std::vector<boost::asio::ip::network_v4> subnetsV4_;
std::vector<boost::asio::ip::network_v6> subnetsV6_;
std::vector<boost::asio::ip::address> ips_;
public:
/**
* @brief Add network address to whitelist
*
* @param net Network part of the ip address
* @throws std::runtime::error when the network address is not valid
*/
void
add(std::string_view net)
{
using namespace boost::asio;
if (not isMask(net))
{
ips_.push_back(ip::make_address(net));
return;
}
if (isV4(net))
subnetsV4_.push_back(ip::make_network_v4(net));
else if (isV6(net))
subnetsV6_.push_back(ip::make_network_v6(net));
else
throw std::runtime_error(fmt::format("malformed network: {}", net.data()));
}
/**
* @brief Checks to see if ip address is whitelisted
*
* @param ip IP address
* @throws std::runtime::error when the network address is not valid
*/
bool
isWhiteListed(std::string_view ip) const
{
using namespace boost::asio;
auto const addr = ip::make_address(ip);
if (std::find(std::begin(ips_), std::end(ips_), addr) != std::end(ips_))
return true;
if (addr.is_v4())
return std::find_if(
std::begin(subnetsV4_), std::end(subnetsV4_), std::bind_front(&isInV4Subnet, std::cref(addr))) !=
std::end(subnetsV4_);
if (addr.is_v6())
return std::find_if(
std::begin(subnetsV6_), std::end(subnetsV6_), std::bind_front(&isInV6Subnet, std::cref(addr))) !=
std::end(subnetsV6_);
return false;
}
private:
static bool
isInV4Subnet(boost::asio::ip::address const& addr, boost::asio::ip::network_v4 const& subnet)
{
auto const range = subnet.hosts();
return range.find(addr.to_v4()) != range.end();
}
static bool
isInV6Subnet(boost::asio::ip::address const& addr, boost::asio::ip::network_v6 const& subnet)
{
auto const range = subnet.hosts();
return range.find(addr.to_v6()) != range.end();
}
bool
isV4(std::string_view net) const
{
static const std::regex ipv4CidrRegex(R"(^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$)");
return std::regex_match(std::string(net), ipv4CidrRegex);
}
bool
isV6(std::string_view net) const
{
static const std::regex ipv6CidrRegex(R"(^([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}/\d{1,3}$)");
return std::regex_match(std::string(net), ipv6CidrRegex);
}
bool
isMask(std::string_view net) const
{
return net.find('/') != std::string_view::npos;
}
};
/**
* @brief A simple handler to add/check elements in a whitelist
*
* @param arr map of net addresses to add to whitelist
*/
class WhitelistHandler
{
Whitelist whitelist_;
public:
WhitelistHandler(clio::Config const& config)
{
std::unordered_set<std::string> arr = getWhitelist(config);
for (auto const& net : arr)
whitelist_.add(net);
}
bool
isWhiteListed(std::string_view ip) const
{
return whitelist_.isWhiteListed(ip);
}
private:
[[nodiscard]] std::unordered_set<std::string> const
getWhitelist(clio::Config const& config) const
{
using T = 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 T{
boost::transform_iterator(std::begin(whitelist), transform),
boost::transform_iterator(std::end(whitelist), transform)};
}
};
} // namespace clio

View File

@@ -24,6 +24,7 @@
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using namespace testing;
using namespace clio;
@@ -38,28 +39,26 @@ constexpr static auto JSONData = R"JSON(
"sweep_interval": 1,
"max_connections": 2,
"max_requests": 3,
"whitelist": ["127.0.0.1"]
}
}
)JSON";
constexpr static auto JSONData2 = R"JSON(
{
"dos_guard": {
"max_fetches": 100,
"sweep_interval": 0.1,
"max_connections": 2,
"whitelist": ["127.0.0.1"]
"whitelist": [
"127.0.0.1"
]
}
}
)JSON";
constexpr static auto IP = "127.0.0.2";
struct MockWhitelistHandler
{
MOCK_METHOD(bool, isWhiteListed, (std::string_view ip), (const));
};
using MockWhitelistHandlerType = NiceMock<MockWhitelistHandler>;
class FakeSweepHandler
{
private:
using guard_type = BasicDOSGuard<FakeSweepHandler>;
using guard_type = BasicDOSGuard<MockWhitelistHandlerType, FakeSweepHandler>;
guard_type* dosGuard_;
public:
@@ -82,13 +81,16 @@ class DOSGuardTest : public NoLoggerFixture
protected:
Config cfg{json::parse(JSONData)};
FakeSweepHandler sweepHandler;
BasicDOSGuard<FakeSweepHandler> guard{cfg, sweepHandler};
MockWhitelistHandlerType whitelistHandler;
BasicDOSGuard<MockWhitelistHandlerType, FakeSweepHandler> guard{cfg, whitelistHandler, sweepHandler};
};
TEST_F(DOSGuardTest, Whitelisting)
{
EXPECT_CALL(whitelistHandler, isWhiteListed("127.0.0.1")).Times(1).WillOnce(Return(false));
EXPECT_FALSE(guard.isWhiteListed("127.0.0.1"));
EXPECT_CALL(whitelistHandler, isWhiteListed("127.0.0.1")).Times(1).WillOnce(Return(true));
EXPECT_TRUE(guard.isWhiteListed("127.0.0.1"));
EXPECT_FALSE(guard.isWhiteListed(IP));
}
TEST_F(DOSGuardTest, ConnectionCount)
@@ -150,28 +152,3 @@ TEST_F(DOSGuardTest, RequestLimitOnTimer)
sweepHandler.sweep();
EXPECT_TRUE(guard.isOk(IP)); // can request again
}
template <typename SweepHandler>
struct BasicDOSGuardMock : public BaseDOSGuard
{
BasicDOSGuardMock(SweepHandler& handler)
{
handler.setup(this);
}
MOCK_METHOD(void, clear, (), (noexcept, override));
};
class DOSGuardIntervalSweepHandlerTest : public SyncAsioContextTest
{
protected:
Config cfg{json::parse(JSONData2)};
IntervalSweepHandler sweepHandler{cfg, ctx};
BasicDOSGuardMock<IntervalSweepHandler> guard{sweepHandler};
};
TEST_F(DOSGuardIntervalSweepHandlerTest, SweepAfterInterval)
{
EXPECT_CALL(guard, clear()).Times(AtLeast(2));
ctx.run_for(std::chrono::milliseconds(400));
}

View File

@@ -22,6 +22,7 @@
#include <rpc/Factories.h>
#include <rpc/common/Specs.h>
#include <rpc/common/Validators.h>
#include <webserver/DOSGuard.h>
#include <boost/json/value.hpp>
#include <boost/json/value_from.hpp>
@@ -175,4 +176,15 @@ struct HandlerWithoutInputMock
MOCK_METHOD(Result, process, (RPC::Context const&), (const));
};
// testing sweep handler by mocking dos guard
template <typename SweepHandler>
struct BasicDOSGuardMock : public clio::BaseDOSGuard
{
BasicDOSGuardMock(SweepHandler& handler)
{
handler.setup(this);
}
MOCK_METHOD(void, clear, (), (noexcept, override));
};
} // namespace unittests::detail

View File

@@ -145,11 +145,13 @@ protected:
boost::asio::io_context ctxSync;
clio::Config cfg{boost::json::parse(JSONData)};
clio::IntervalSweepHandler sweepHandler = clio::IntervalSweepHandler{cfg, ctxSync};
clio::DOSGuard dosGuard = clio::DOSGuard{cfg, sweepHandler};
clio::WhitelistHandler whitelistHandler = clio::WhitelistHandler{cfg};
clio::DOSGuard dosGuard = clio::DOSGuard{cfg, whitelistHandler, sweepHandler};
clio::Config cfgOverload{boost::json::parse(JSONDataOverload)};
clio::IntervalSweepHandler sweepHandlerOverload = clio::IntervalSweepHandler{cfgOverload, ctxSync};
clio::DOSGuard dosGuardOverload = clio::DOSGuard{cfgOverload, sweepHandlerOverload};
clio::WhitelistHandler whitelistHandlerOverload = clio::WhitelistHandler{cfgOverload};
clio::DOSGuard dosGuardOverload = clio::DOSGuard{cfgOverload, whitelistHandlerOverload, sweepHandlerOverload};
// this ctx is for http server
boost::asio::io_context ctx;

View File

@@ -0,0 +1,54 @@
//------------------------------------------------------------------------------
/*
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 <config/Config.h>
#include <rpc/handlers/impl/FakesAndMocks.h>
#include <util/Fixtures.h>
#include <webserver/DOSGuard.h>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
using namespace clio;
using namespace testing;
constexpr static auto JSONData = R"JSON(
{
"dos_guard": {
"max_fetches": 100,
"sweep_interval": 0.1,
"max_connections": 2,
"whitelist": ["127.0.0.1"]
}
}
)JSON";
class DOSGuardIntervalSweepHandlerTest : public SyncAsioContextTest
{
protected:
Config cfg{boost::json::parse(JSONData)};
IntervalSweepHandler sweepHandler{cfg, ctx};
unittests::detail::BasicDOSGuardMock<IntervalSweepHandler> guard{sweepHandler};
};
TEST_F(DOSGuardIntervalSweepHandlerTest, SweepAfterInterval)
{
EXPECT_CALL(guard, clear()).Times(AtLeast(2));
ctx.run_for(std::chrono::milliseconds(400));
}

View File

@@ -0,0 +1,75 @@
//------------------------------------------------------------------------------
/*
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 <config/Config.h>
#include <util/Fixtures.h>
#include <webserver/DOSGuard.h>
#include <boost/json/parse.hpp>
#include <gmock/gmock.h>
using namespace clio;
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
{
};
TEST_F(WhitelistHandlerTest, TestWhiteListIPV4)
{
Config cfg{boost::json::parse(JSONDataIPV4)};
WhitelistHandler whitelistHandler{cfg};
EXPECT_TRUE(whitelistHandler.isWhiteListed("192.168.1.10"));
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)
{
Config cfg{boost::json::parse(JSONDataIPV6)};
WhitelistHandler whitelistHandler{cfg};
EXPECT_TRUE(whitelistHandler.isWhiteListed("2002:1dd8:85a7:0000:0000:8a6e:0000:1111"));
EXPECT_FALSE(whitelistHandler.isWhiteListed("2002:1dd8:85a7:1101:0000:8a6e:0000:1111"));
EXPECT_TRUE(whitelistHandler.isWhiteListed("2001:0db8:85a3:0000:0000:8a2e:0000:0000"));
EXPECT_TRUE(whitelistHandler.isWhiteListed("2001:0db8:85a3:0000:1111:8a2e:0370:7334"));
}