[MEDIUM] implement "rate-limit sessions" for the frontend

The new "rate-limit sessions" statement sets a limit on the number of
new connections per second on the frontend. As it is extremely accurate
(about 0.1%), it is efficient at limiting resource abuse or DoS.
This commit is contained in:
Willy Tarreau 2009-03-05 23:48:25 +01:00
parent 079ff0a207
commit 3a7d20781d
4 changed files with 129 additions and 16 deletions

View File

@ -617,6 +617,7 @@ option tcpka X X X X
option tcplog X X X X
[no] option tcpsplice X X X X
[no] option transparent X - X X
rate-limit sessions X X X -
redirect - X X X
redisp X - X X (deprecated)
redispatch X - X X (deprecated)
@ -2573,6 +2574,39 @@ no option transparent
"transparent" option of the "bind" keyword.
rate-limit sessions <rate>
Set a limit on the number of new sessions accepted per second on a frontend
May be used in sections : defaults | frontend | listen | backend
yes | yes | yes | no
Arguments :
<rate> The <rate> parameter is an integer designating the maximum number
of new sessions per second to accept on the frontend.
When the frontend reaches the specified number of new sessions per second, it
stops accepting new connections until the rate drops below the limit again.
During this time, the pending sessions will be kept in the socket's backlog
(in system buffers) and haproxy will not even be aware that sessions are
pending. When applying very low limit on a highly loaded service, it may make
sense to increase the socket's backlog using the "backlog" keyword.
This feature is particularly efficient at blocking connection-based attacks
or service abuse on fragile servers. Since the session rate is measured every
millisecond, it is extremely accurate. Also, the limit applies immediately,
no delay is needed at all to detect the threshold.
Example : limit the connection rate on SMTP to 10 per second max
listen smtp
mode tcp
bind :25
rate-limit sessions 10
server 127.0.0.1:1025
Note : when the maximum rate is reached, the frontend's status appears as
"FULL" in the statistics, exactly as when it is saturated.
See also : the "backlog" keyword and the "fe_sess_rate" ACL criterion.
redirect location <to> [code <code>] <option> {if | unless} <condition>
redirect prefix <to> [code <code>] <option> {if | unless} <condition>
Return an HTTP redirection if/unless a condition is matched

View File

@ -226,6 +226,7 @@ struct proxy {
unsigned int cum_feconn, cum_beconn; /* cumulated number of processed sessions */
unsigned int cum_lbconn; /* cumulated number of sessions processed by load balancing */
unsigned int maxconn; /* max # of active sessions on the frontend */
unsigned int fe_maxsps; /* max # of new sessions per second on the frontend */
unsigned int fullconn; /* #conns on backend above which servers are used at full load */
struct in_addr except_net, except_mask; /* don't x-forward-for for this address. FIXME: should support IPv6 */
char *fwdfor_hdr_name; /* header to use - default: "x-forwarded-for" */

View File

@ -70,7 +70,9 @@ int event_accept(int fd) {
int cfd;
int max_accept = global.tune.maxaccept;
while (p->feconn < p->maxconn && max_accept--) {
while (p->feconn < p->maxconn &&
(!p->fe_maxsps || read_freq_ctr(&p->fe_sess_per_sec) < p->fe_maxsps) &&
max_accept--) {
struct sockaddr_storage addr;
socklen_t laddr = sizeof(addr);

View File

@ -1,7 +1,7 @@
/*
* Proxy variables and functions.
*
* Copyright 2000-2008 Willy Tarreau <w@1wt.eu>
* Copyright 2000-2009 Willy Tarreau <w@1wt.eu>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -169,6 +169,68 @@ static int proxy_parse_timeout(char **args, int section, struct proxy *proxy,
return retval;
}
/* This function parses a "rate-limit" statement in a proxy section. It returns
* -1 if there is any error, 1 for a warning, otherwise zero. If it does not
* return zero, it may write an error message into the <err> buffer, for at
* most <errlen> bytes, trailing zero included. The trailing '\n' must not
* be written. The function must be called with <args> pointing to the first
* command line word, with <proxy> pointing to the proxy being parsed, and
* <defpx> to the default proxy or NULL.
*/
static int proxy_parse_rate_limit(char **args, int section, struct proxy *proxy,
struct proxy *defpx, char *err, int errlen)
{
int retval, cap;
char *res, *name;
unsigned int *tv = NULL;
unsigned int *td = NULL;
unsigned int val;
retval = 0;
/* simply skip "rate-limit" */
if (strcmp(args[0], "rate-limit") == 0)
args++;
name = args[0];
if (!strcmp(args[0], "sessions")) {
name = "sessions";
tv = &proxy->fe_maxsps;
td = &defpx->fe_maxsps;
cap = PR_CAP_FE;
} else {
snprintf(err, errlen,
"%s '%s': must be 'sessions'",
"rate-limit", args[0]);
return -1;
}
if (*args[1] == 0) {
snprintf(err, errlen, "%s %s expects expects an integer value (in sessions/second)", "rate-limit", name);
return -1;
}
val = strtoul(args[1], &res, 0);
if (*res) {
snprintf(err, errlen, "%s %s: unexpected character '%c' in integer value '%s'", "rate-limit", name, *res, args[1]);
return -1;
}
if (!(proxy->cap & cap)) {
snprintf(err, errlen, "%s %s will be ignored because %s '%s' has no %s capability",
"rate-limit", name, proxy_type_str(proxy), proxy->id,
(cap & PR_CAP_BE) ? "backend" : "frontend");
retval = 1;
}
else if (defpx && *tv != *td) {
snprintf(err, errlen, "overwriting %s %s which was already specified", "rate-limit", name);
retval = 1;
}
*tv = val;
return retval;
}
/*
* This function finds a proxy with matching name, mode and with satisfying
* capabilities. It also checks if there are more matching proxies with
@ -313,22 +375,35 @@ void maintain_proxies(int *next)
/* if there are enough free sessions, we'll activate proxies */
if (actconn < global.maxconn) {
while (p) {
if (!p->maxconn || p->feconn < p->maxconn) {
if (p->state == PR_STIDLE) {
for (l = p->listen; l != NULL; l = l->next)
enable_listener(l);
p->state = PR_STRUN;
}
for (; p; p = p->next) {
/* check the various reasons we may find to block the frontend */
if (p->feconn >= p->maxconn)
goto do_block;
if (p->fe_maxsps && read_freq_ctr(&p->fe_sess_per_sec) >= p->fe_maxsps) {
/* we're blocking because a limit was reached on the number of
* requests/s on the frontend. We want to re-check ASAP, which
* means in 1 ms because the timer will have settled down. Note
* that we may already be in IDLE state here.
*/
*next = tick_first(*next, tick_add(now_ms, 1));
goto do_block;
}
else {
if (p->state == PR_STRUN) {
for (l = p->listen; l != NULL; l = l->next)
disable_listener(l);
p->state = PR_STIDLE;
}
/* OK we have no reason to block, so let's unblock if we were blocking */
if (p->state == PR_STIDLE) {
for (l = p->listen; l != NULL; l = l->next)
enable_listener(l);
p->state = PR_STRUN;
}
continue;
do_block:
if (p->state == PR_STRUN) {
for (l = p->listen; l != NULL; l = l->next)
disable_listener(l);
p->state = PR_STIDLE;
}
p = p->next;
}
}
else { /* block all proxies */
@ -525,6 +600,7 @@ static struct cfg_kw_list cfg_kws = {{ },{
{ CFG_LISTEN, "clitimeout", proxy_parse_timeout },
{ CFG_LISTEN, "contimeout", proxy_parse_timeout },
{ CFG_LISTEN, "srvtimeout", proxy_parse_timeout },
{ CFG_LISTEN, "rate-limit", proxy_parse_rate_limit },
{ 0, NULL, NULL },
}};