HAProxy – Slowing down abuse with user friendly rate controls

HAProxy – Slowing down abuse with user friendly rate controls

There are various situations where clients can overload a website server, but you don’t want to return a 4xx or 5xx simply because the system is getting bogged down, instead it would be better to have a mechanism which tries to take the pressure off the backend server to give all clients a fair chance. Thankfully HAProxy has a couple of ways that requests can be slowed down on their way to the backend, the cleanest mechanism is to use a small lua script ( HAProxy 1.6.0+ )

The fun comes when looking at how to trigger this delay and the example below uses a few different triggers to catch different scenarios.

/etc/haproxy/delay.lua

function delay_request(txn)
    core.msleep(1500 + math.random(1000))
end
core.register_action("delay_request", { "http-req" }, delay_request);

/etc/haproxy/haproxy.cfg

global
    lua-load /etc/haproxy/delay.lua

backend Abuse
    stick-table type ip size 100k expire 1m store gpc0,conn_cur,conn_rate(3s),http_req_rate(10s),http_err_rate(20s)

backend Abuse_Req
   stick-table type ip size 1m expire 12h store gpc0,gpc0_rate(5s)

frontend VIP1
    bind 1.2.3.4:80

    acl acl_ua_blacklist hdr_sub(User-Agent) -i slurp
    acl acl_ua_blacklist hdr_sub(User-Agent) -i spider
    acl acl_ua_blacklist hdr_sub(User-Agent) -i bot

    acl acl_ignore_this  path_end -i .png
    acl acl_ignore_this  path_end -i .jpg
    acl acl_ignore_this  path_end -i .jpeg
    acl acl_ignore_this  path_end -i .gif
    acl acl_ignore_this  path_end -i .woff
    acl acl_ignore_this  path_end -i .otf
    acl acl_ignore_this  path_end -i .ttf
    acl acl_ignore_this  path_end -i .svg
    acl acl_ignore_this  path_end -i .eot
    acl acl_ignore_this  path_end -i .css
    acl acl_ignore_this  path_end -i .js

    tcp-request connection track-sc0 src table Abuse

    acl acl_abuse sc0_conn_rate(Abuse) ge 10
    acl acl_abuse sc0_http_req_rate(Abuse) ge 100
    acl acl_abuse sc0_http_err_rate(Abuse) ge 20
    ## sc1_gpc0_rate = request rate specific to requests which get through the acl_ignore_this filter below
    acl acl_abuse sc1_gpc0_rate(Abuse_Req) ge 10

    acl acl_flag_abuser sc0_inc_gpc0(Abuse) ge 0

    ## delay requests when abuse is detected
    http-request lua.delay_request if !acl_ignore_this { sc1_inc_gpc0(Abuse_Req) gt 0 } acl_abuse acl_flag_abuser

    ## continue to delay all requests for the duration of the table expire time
    http-request lua.delay_request if { sc0_get_gpc0(Abuse) gt 0 }

    ## delay requests when there are more than 10 concurrent connections
    http-request lua.delay_request if { sc0_conn_cur(Abuse) ge 10 }

    ## delay requests from Spider Bots
    http-request lua.delay_request if acl_ua_blacklist

The above User-Agent blacklist has a very relaxed matching rule “bot” but because this setup does not deny requests and just adds the small delay to each request it would not matter too much if this ACL matched to something it should not have.

The general purpose counter gpc0 is used to record that abuse has been detected for the specific source IP

http-request lua.delay_request  if !acl_ignore_this { sc1_inc_gpc0(Abuse_Req) gt 0 } acl_abuse acl_flag_abuser

if the request is not in the acl_ignore_this ACL then increment the gpc0 in the sc1 table. Then acl_abuse contains the actually trigger levels for detecting abuse and it’s only when acl_abuse returns true that haproxy will call acl_flag_abuser which then increments the gpc0 counter and also returns true resulting in the request being delayed.

http-request lua.delay_request if { sc0_get_gpc0(Abuse) gt 0 }

This continues to delay requests while the gpc0 counter is greater than 0, which will be true for any IPs which have previously been flagged by one of the acl_abuse rules. This state will continue until the table entry has expired.

sc1_gpc0(Abuse_Req) is also useful for reporting the clients who make the most dynamic html and json calls in the last 12h, which is why this table has such a high expire time

View the Abuse table rows

root@lb1:~# echo "show table Abuse" | socat unix-connect:/run/haproxy/admin.sock stdio
# table: Abuse, type: ip, size:102400, used:380
0x20e1b1c: key=81.153.52.70 use=0 exp=33074 gpc0=0 conn_rate(3000)=0 conn_cur=0 http_req_rate(10000)=0 http_err_rate(20000)=0
0x210b62c: key=82.132.236.240 use=0 exp=23402 gpc0=0 conn_rate(3000)=0 conn_cur=0 http_req_rate(10000)=0 http_err_rate(20000)=0
0x1fa0c3c: key=82.132.242.131 use=0 exp=52647 gpc0=0 conn_rate(3000)=0 conn_cur=0 http_req_rate(10000)=3 http_err_rate(20000)=0
0x209ac9c: key=85.255.234.39 use=0 exp=41840 gpc0=0 conn_rate(3000)=0 conn_cur=0 http_req_rate(10000)=0 http_err_rate(20000)=0
0x214d27c: key=86.113.162.57 use=0 exp=23190 gpc0=0 conn_rate(3000)=0 conn_cur=0 http_req_rate(10000)=0 http_err_rate(20000)=0
...

View the Abuse_Req table – Top Requests

root@lb1:~# echo "show table Abuse_Req" | socat unix-connect:/run/haproxy/admin.sock stdio  | sed -e "s/gpc0=//" | sort -t" " -nk5 | tail -10 
0x111c430: key=5.80.128.235 use=0 exp=42921961 21 gpc0_rate(5000)=0
0x1015340: key=109.153.251.211 use=0 exp=42562885 22 gpc0_rate(5000)=0
0xf5c5a0: key=80.229.228.105 use=0 exp=42974321 22 gpc0_rate(5000)=0
0xf5bed0: key=90.198.127.228 use=0 exp=43145733 23 gpc0_rate(5000)=0
0xfb81a0: key=92.40.249.168 use=0 exp=43030832 25 gpc0_rate(5000)=0
0x1085760: key=188.29.165.159 use=0 exp=43153401 28 gpc0_rate(5000)=0
0x1121ce0: key=94.197.120.126 use=0 exp=43064693 28 gpc0_rate(5000)=0
0xfebec0: key=79.69.141.143 use=0 exp=42881105 31 gpc0_rate(5000)=0
0x1129cb0: key=147.148.167.129 use=0 exp=42926477 46 gpc0_rate(5000)=0
0xf98e00: key=86.145.59.214 use=0 exp=42937952 48 gpc0_rate(5000)=0

References:
godevops.net/2015/06/24/adding-random-delay-specific-http-requests-haproxy-lua/
gist.github.com/jeremyj/e964a951634f1997daea
www.loadbalancer.org/blog/simple-denial-of-service-dos-attack-mitigation-using-haproxy-2

Comments are closed.