MXS-2414: Prototype connection attempt throttling

The RateLimit class stores authentication failure data mapped by the
client IP addresses. The authentication failures are limited
per thread. The limits are still hard-coded and at least the number of
failures should be made configurable.

The simplest, most maintainable and acceptably efficient implementation
for DDoS protection is a thread-local unordered_map. The unwanted
side-effect of "scaling" of the number of allowed authentication failures
is unlikely to be problematic in most use-cases.

As the blocking of a host is only temporary, the behavior differs from the
one in the MariaDB server. This allows the number of failures to be set to
a much lower value negating some of the problems caused by the relatively
simple implementation.
This commit is contained in:
Markus Mäkelä 2019-04-18 16:21:36 +03:00
parent d1ab4fcb89
commit cf86b0cb7e
No known key found for this signature in database
GPG Key ID: 72D48FCE664F7B19
3 changed files with 77 additions and 0 deletions

View File

@ -186,6 +186,16 @@ public:
return m_type;
}
/**
* Mark authentication as failed
*
* This updates the number of failures that have occurred from this host. If the number of authentications
* exceeds a certain value, any attempts to connect from the remote in quesion will be rejected.
*
* @param remote The address where the connection originated
*/
void mark_auth_as_failed(const std::string& remote);
// Functions that are temporarily public
bool create_listener_config(const char* filename);
struct users* users() const;

View File

@ -21,6 +21,7 @@
#include <sys/stat.h>
#include <algorithm>
#include <chrono>
#include <list>
#include <memory>
#include <mutex>
@ -40,6 +41,9 @@
#include "internal/modules.hh"
#include "internal/session.hh"
using Clock = std::chrono::steady_clock;
using std::chrono::seconds;
static std::list<SListener> all_listeners;
static std::mutex listener_lock;
@ -47,6 +51,56 @@ static RSA* rsa_512 = NULL;
static RSA* rsa_1024 = NULL;
static RSA* tmp_rsa_callback(SSL* s, int is_export, int keylength);
// TODO: Make these configurable
constexpr int MAX_FAILURES = 25;
constexpr int BLOCK_TIME = 60;
namespace
{
class RateLimit
{
public:
void auth_failed(const std::string& remote)
{
auto& u = m_failures[remote];
u.last_failure = Clock::now();
u.failures++;
}
bool is_blocked(const std::string& remote)
{
bool rval = false;
auto it = m_failures.find(remote);
if (it != m_failures.end())
{
auto& u = it->second;
if (Clock::now() - u.last_failure > seconds(BLOCK_TIME))
{
u.last_failure = Clock::now();
u.failures = 0;
}
rval = u.failures >= MAX_FAILURES;
}
return rval;
}
private:
struct Failure
{
Clock::time_point last_failure = Clock::now();
uint32_t failures = 0;
};
std::unordered_map<std::string, Failure> m_failures;
};
thread_local RateLimit rate_limit;
}
Listener::Listener(SERVICE* service, const std::string& name, const std::string& address,
uint16_t port, const std::string& protocol, const std::string& authenticator,
const std::string& auth_opts, void* auth_instance, SSL_LISTENER* ssl)
@ -968,6 +1022,12 @@ static ClientConn accept_one_connection(int fd)
}
configure_network_socket(conn.fd, conn.addr.ss_family);
if (rate_limit.is_blocked(conn.host))
{
close(conn.fd);
conn.fd = -1;
}
}
else if (errno != EAGAIN && errno != EWOULDBLOCK)
{
@ -1177,3 +1237,8 @@ void Listener::accept_connections()
}
}
}
void Listener::mark_auth_as_failed(const std::string& remote)
{
rate_limit.auth_failed(remote);
}

View File

@ -818,6 +818,8 @@ static int gw_read_do_authentication(DCB* dcb, GWBUF* read_buffer, int nbytes_re
{
protocol->protocol_auth_state = MXS_AUTH_STATE_FAILED;
mysql_client_auth_error_handling(dcb, auth_val, next_sequence);
mxb_assert(dcb->session->listener);
dcb->session->listener->mark_auth_as_failed(dcb->remote);
/**
* Close DCB and which will release MYSQL_session
*/