]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
[MEDIUM] implement "rate-limit sessions" for the frontend
authorWilly Tarreau <w@1wt.eu>
Thu, 5 Mar 2009 22:48:25 +0000 (23:48 +0100)
committerWilly Tarreau <w@1wt.eu>
Thu, 5 Mar 2009 22:48:25 +0000 (23:48 +0100)
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.

doc/configuration.txt
include/types/proxy.h
src/client.c
src/proxy.c

index 24bf1bc861b42162e9e760db214282d83f88f0a9..d5a2c915313e77de18a7713d4d7a4608cb20583c 100644 (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
index 87b6f14f56a9415a277c735c96a316111c51ffcc..432b27bb3e5b9477ca029a12d6f4b839384fad5f 100644 (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" */
index 3e1ff58404a75bab14c0848ab45fbf18db5a8a8f..f6ea7466a698a58c42ddd2f5a7b968c7463ba1dd 100644 (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);
 
index 5fa202bd3cb31a11c3b2774add67c766332f43ae..3e6854ddc277c8fa2df7add24f834c689edd896e 100644 (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 },
 }};