Implement rate limiting (#43)
* Implement ratelimiting
- by sending back '429 Too Many Requests' error message
- Error message is a HTTP/1.1 response, so contrail link local proxy works with it too.
- HAProxy sends back HTTP/1.0 responses, so using a template is required.
Only possible when using front-/backend configuration layout.
* Add options in frontend and backend.
Options are filtered by a blacklist of invalid options, as per haproxy manual.
* Add sls test for rate limiting configuration
* Update readme with rate limiting example
diff --git a/README.rst b/README.rst
index 06fd886..09bcdd7 100644
--- a/README.rst
+++ b/README.rst
@@ -427,6 +427,30 @@
port: 8443
params: 'maxconn 256'
+Implement rate limiting, to prevent excessive requests
+This feature only works if using 'format: end'
+
+.. code-block:: yaml
+ haproxy:
+ proxy:
+ ...
+ listen:
+ nova_metadata_api:
+ ...
+ format: end
+ options:
+ - httpchk
+ - httpclose
+ - httplog
+ rate_limit:
+ duration: 900s
+ enabled: true
+ requests: 125
+ track: content
+ servers:
+ ...
+ type: http
+
Read more
=========
diff --git a/haproxy/files/errors/429.http11 b/haproxy/files/errors/429.http11
new file mode 100644
index 0000000..6ba5890
--- /dev/null
+++ b/haproxy/files/errors/429.http11
@@ -0,0 +1,9 @@
+HTTP/1.1 429 Too Many Requests
+Cache-Control: no-cache
+Connection: close
+Content-Type: text/html
+Content-Length: 118
+
+<html><body><h1>429 Too Many Requests</h1>
+You have sent too many requests in a given amount of time.
+</body></html>
diff --git a/haproxy/files/haproxy.cfg b/haproxy/files/haproxy.cfg
index f86f379..4f9bf0d 100644
--- a/haproxy/files/haproxy.cfg
+++ b/haproxy/files/haproxy.cfg
@@ -1,4 +1,4 @@
-{%- from "haproxy/map.jinja" import proxy with context -%}
+{%- from "haproxy/map.jinja" import proxy, invalid_section_options with context -%}
global
{%- if proxy.nbproc is defined %}
@@ -232,25 +232,53 @@
redirect {% if redirect.code is defined %} code {{ redirect.code }} {% endif %} location {{ redirect.location }} if { {{ condition.type }} {{ condition.condition }} }
{%- endfor %}
{%- endfor %}
+
+ {#- Add options in the frontend section that make sense. #}
+ {%- for option in listen.get('options', []) %}
+ {%- if option not in invalid_section_options.frontend %}
+ option {{ option }}
+ {%- endif %}
+ {%- endfor %}
+
{%- for acl in listen.get('acls', []) %}
{%- for condition in acl.get('conditions', []) %}
acl {{ acl.name }} {{ condition.type }} {{ condition.condition }}
{%- endfor %}
+
{%- if listen_name == 'service_proxy' %}
use_backend {{ acl.backend|default(acl.name, true) }} if {{ acl.name }}
{% else %}
use_backend {{ acl.name }}-backend if {{ acl.name }}
{% endif %}
{%- endfor %}
+
+ {%- if listen.rate_limit is defined and listen.rate_limit.get('enabled', False) %}
+ tcp-request inspect-delay 5s
+ acl too_many_requests sc0_gpc0_rate() gt {{ listen.rate_limit.get('requests', 100) }}
+ acl mark_seen sc0_inc_gpc0 gt 0
+ stick-table type string size 100k store gpc0_rate({{ listen.rate_limit.get('duration', '60s') }})
+ {%- if listen.rate_limit.get('track', 'content') == 'content' %}
+ tcp-request content track-sc0 {{ listen.rate_limit.get('header', 'hdr(X-Forwarded-For)') }} if ! too_many_requests
+ {%- else %}
+ tcp-request connection track-sc0 {{ listen.rate_limit.get('tracking_key', 'src') }} if ! too_many_requests
+ {%- endif %}
+ use_backend {{ listen_name }}-rate_limit if mark_seen too_many_requests
+ {%- endif %}
+
default_backend {{ listen_name }}-backend
backend {{ listen_name }}-backend
{%- if listen.get('type', None) == 'http' %}
balance {{ listen.get('balance', 'roundrobin') }}
{%- endif %}
+
+ {#- Add options in the backend section that make sense. #}
{%- for option in listen.get('options', []) %}
+ {%- if option not in invalid_section_options.backend %}
option {{ option }}
+ {%- endif %}
{%- endfor %}
+
{%- for server in listen.get('servers', []) %}
server {{ server.get('name', server.host) }} {{ server.host }}:{{ server.port }} {{ server.get('params', '') }}
{%- endfor %}
@@ -267,5 +295,13 @@
{%- endfor %}
{%- endfor %}
{%- endif %}
+
+{%- if listen.rate_limit is defined and listen.rate_limit.get('enabled', False) %}
+
+backend {{ listen_name }}-rate_limit
+ timeout tarpit {{ listen.rate_limit.get('tarpit_timeout', '2s') }}
+ errorfile 500 /etc/haproxy/errors/429.http11
+ http-request tarpit
+{%- endif %}
{%- endif %}
{%- endfor %}
diff --git a/haproxy/map.jinja b/haproxy/map.jinja
index 2fdb2b3..966f342 100644
--- a/haproxy/map.jinja
+++ b/haproxy/map.jinja
@@ -10,3 +10,45 @@
'stats_socket': '/var/lib/haproxy/stats',
},
}, merge=salt['pillar.get']('haproxy:proxy')) %}
+{% set invalid_section_options = {
+ 'frontend': [
+ 'abortonclose',
+ 'accept-invalid-http-response',
+ 'allbackups',
+ 'checkcache',
+ 'external-check',
+ 'httpchk',
+ 'ldap-check',
+ 'log-health-checks',
+ 'mysql-check',
+ 'persist',
+ 'pgsql-check',
+ 'prefer-last-server',
+ 'redis-check',
+ 'redispatch',
+ 'smtpchk',
+ 'srvtcpka',
+ 'ssl-hello-chk',
+ 'tcp-check',
+ 'tcp-smart-connect',
+ 'tcpka',
+ 'tcplog',
+ 'transparent',
+ ],
+ 'backend': [
+ 'accept-invalid-http-request',
+ 'clitcpka',
+ 'contstats',
+ 'dontlog-normal',
+ 'dontlognull',
+ 'http-ignore-probes',
+ 'http-use-proxy-header',
+ 'log-separate-errors',
+ 'logasap',
+ 'socket-stats',
+ 'tcp-smart-accept',
+ 'tcpka',
+ 'tcplog',
+ ],
+}
+%}
diff --git a/haproxy/proxy.sls b/haproxy/proxy.sls
index 55792c6..07be51a 100644
--- a/haproxy/proxy.sls
+++ b/haproxy/proxy.sls
@@ -27,6 +27,17 @@
- require:
- pkg: haproxy_packages
+rate_limit_error_file:
+ file.managed:
+ - name: /etc/haproxy/errors/429.http11
+ - user: root
+ - group: root
+ - mode: 644
+ - source: salt://haproxy/files/errors/429.http11
+ - template: jinja
+ - require:
+ - pkg: haproxy_packages
+
haproxy_status_packages:
pkg.installed:
- pkgs:
diff --git a/tests/pillar/single_rate_limiting.sls b/tests/pillar/single_rate_limiting.sls
new file mode 100644
index 0000000..fe13de9
--- /dev/null
+++ b/tests/pillar/single_rate_limiting.sls
@@ -0,0 +1,40 @@
+haproxy:
+ proxy:
+ enabled: true
+ mode: tcp
+ logging: syslog
+ max_connections: 1024
+ listen:
+ nova_metadata_api:
+ binds:
+ - address: 127.0.0.1
+ port: 8775
+ format: end
+ options:
+ - httpchk
+ - httpclose
+ - httplog
+ rate_limit:
+ duration: 900s
+ enabled: true
+ requests: 125
+ track: content
+ servers:
+ - host: 127.0.0.1
+ name: ctl01
+ params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+ port: 8775
+ - host: 127.0.0.1
+ name: ctl02
+ params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+ port: 8775
+ - host: 127.0.0.1
+ name: ctl03
+ params: check inter 10s fastinter 2s downinter 3s rise 3 fall 3
+ port: 8775
+ type: http
+
+# For haproxy/meta/sensu.yml
+linux:
+ network:
+ fqdn: linux.ci.local