From d595fadfd8cf66406d55e43b3b6ef4329a1e0782 Mon Sep 17 00:00:00 2001 From: Tamara Boehm Date: Fri, 27 Feb 2026 10:24:00 +0100 Subject: [PATCH] Introduce a cidr list to exclude from connection rate limiting --- jobs/haproxy/spec | 8 +++ .../cidrs_to_exclude_from_blocking.txt.erb | 25 ++++++++ jobs/haproxy/templates/haproxy.config.erb | 6 +- ...cidrs_to_exclude_from_blocking.txt_spec.rb | 63 +++++++++++++++++++ .../haproxy_config/rate_limit_spec.rb | 27 +++++++- 5 files changed, 124 insertions(+), 5 deletions(-) create mode 100644 jobs/haproxy/templates/cidrs_to_exclude_from_blocking.txt.erb create mode 100644 spec/haproxy/templates/cidrs_to_exclude_from_blocking.txt_spec.rb diff --git a/jobs/haproxy/spec b/jobs/haproxy/spec index 0ec512a8..a23f8422 100644 --- a/jobs/haproxy/spec +++ b/jobs/haproxy/spec @@ -24,6 +24,7 @@ templates: blacklist_cidrs.txt.erb: config/blacklist_cidrs.txt whitelist_cidrs.txt.erb: config/whitelist_cidrs.txt expect_proxy_cidrs.txt.erb: config/expect_proxy_cidrs.txt + cidrs_to_exclude_from_blocking.txt.erb: config/cidrs_to_exclude_from_blocking.txt trusted_domain_cidrs.txt.erb: config/trusted_domain_cidrs.txt consumes: @@ -816,6 +817,13 @@ properties: description: Window size for counting connections. See docs/rate_limiting.md ha_proxy.connections_rate_limit.table_size: description: Size of the stick table in which the IPs and counters are stored. See docs/rate_limiting.md + ha_proxy.connections_rate_limit.cidrs_to_exclude: + description: List of CIDRs to exclude during connection rate limiting. Format is a string array of CIDRs or a single string of base64 encoded gzip. + default: ~ + example: + cidrs_to_exclude: + - 10.0.0.0/8 + - 192.168.2.0/32 ha_proxy.connections_rate_limit.block: description: Whether or not to block connections. See docs/rate_limiting.md default: false diff --git a/jobs/haproxy/templates/cidrs_to_exclude_from_blocking.txt.erb b/jobs/haproxy/templates/cidrs_to_exclude_from_blocking.txt.erb new file mode 100644 index 00000000..b1a01fac --- /dev/null +++ b/jobs/haproxy/templates/cidrs_to_exclude_from_blocking.txt.erb @@ -0,0 +1,25 @@ +# generated from cidrs_to_exclude_from_blocking.txt.erb +<% +require "base64" +require 'zlib' +require 'stringio' + +if_p("ha_proxy.connections_rate_limit.cidrs_to_exclude") do |cidrs| + uncompressed = '' + if cidrs.is_a?(Array) + uncompressed << "\# detected cidrs provided as array in cleartext format\n" + cidrs.each do |cidr| + uncompressed << cidr << "\n" + end + else + gzplain = Base64.decode64(cidrs) + gz = Zlib::GzipReader.new(StringIO.new(gzplain)) + uncompressed = gz.read + end +%> +# BEGIN cidrs to exclude from tcp rejection because of connection rate limiting +<%= uncompressed %> +# END cidrs to exclude from tcp rejection because of connection rate limiting +<% +end +%> diff --git a/jobs/haproxy/templates/haproxy.config.erb b/jobs/haproxy/templates/haproxy.config.erb index bbe8b578..e4d83cc4 100644 --- a/jobs/haproxy/templates/haproxy.config.erb +++ b/jobs/haproxy/templates/haproxy.config.erb @@ -432,7 +432,8 @@ frontend http-in tcp-request connection track-sc0 src table st_tcp_conn_rate <%- if_p("ha_proxy.connections_rate_limit.block", "ha_proxy.connections_rate_limit.connections") do |block, connections| -%> <%-if block -%> - tcp-request connection reject if { sc_conn_rate(0) gt <%= connections %> } + acl cidr_list_to_exclude src -f /var/vcap/jobs/haproxy/config/cidrs_to_exclude_from_blocking.txt + tcp-request connection reject if { sc_conn_rate(0) gt <%= connections %> } !cidr_list_to_exclude <%- end -%> <%- end -%> <%- end -%> @@ -564,7 +565,8 @@ frontend https-in tcp-request connection track-sc0 src table st_tcp_conn_rate <%- if_p("ha_proxy.connections_rate_limit.block", "ha_proxy.connections_rate_limit.connections") do |block, connections| -%> <%-if block -%> - tcp-request connection reject if { sc_conn_rate(0) gt <%= connections %> } + acl cidr_list_to_exclude src -f /var/vcap/jobs/haproxy/config/cidrs_to_exclude_from_blocking.txt + tcp-request connection reject if { sc_conn_rate(0) gt <%= connections %> } !cidr_list_to_exclude <%- end -%> <%- end -%> <%- end -%> diff --git a/spec/haproxy/templates/cidrs_to_exclude_from_blocking.txt_spec.rb b/spec/haproxy/templates/cidrs_to_exclude_from_blocking.txt_spec.rb new file mode 100644 index 00000000..c75620cf --- /dev/null +++ b/spec/haproxy/templates/cidrs_to_exclude_from_blocking.txt_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'rspec' + +describe 'config/cidrs_to_exclude_from_blocking.txt' do + let(:template) { haproxy_job.template('config/cidrs_to_exclude_from_blocking.txt') } + + context 'when ha_proxy.connections_rate_limit.cidrs_to_exclude is provided' do + context 'when an array of cidrs is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'connections_rate_limit' => { + 'window_size' => '10s', + 'table_size' => '10m', + 'cidrs_to_exclude' => ['10.0.0.0/8', '3.22.12.3/32'] + } + } + })).to eq(<<~EXPECTED) + # generated from cidrs_to_exclude_from_blocking.txt.erb + + # BEGIN cidrs to exclude from tcp rejection because of connection rate limiting + # detected cidrs provided as array in cleartext format + 10.0.0.0/8 + 3.22.12.3/32 + + # END cidrs to exclude from tcp rejection because of connection rate limiting + + EXPECTED + end + end + + context 'when a base64-encoded, gzipped config is provided' do + it 'has the correct contents' do + expect(template.render({ + 'ha_proxy' => { + 'connections_rate_limit' => { + 'cidrs_to_exclude' => gzip_and_b64_encode(<<~INPUT) + 10.0.0.0/8 + 3.22.12.3/32 + INPUT + } + } + })).to eq(<<~EXPECTED) + # generated from cidrs_to_exclude_from_blocking.txt.erb + + # BEGIN cidrs to exclude from tcp rejection because of connection rate limiting + 10.0.0.0/8 + 3.22.12.3/32 + + # END cidrs to exclude from tcp rejection because of connection rate limiting + + EXPECTED + end + end + end + + context 'when ha_proxy.connections_rate_limit.cidrs_to_exclude is not provided' do + it 'is empty' do + expect(template.render({})).to be_a_blank_string + end + end +end diff --git a/spec/haproxy/templates/haproxy_config/rate_limit_spec.rb b/spec/haproxy/templates/haproxy_config/rate_limit_spec.rb index 5baddf8d..84d95a38 100644 --- a/spec/haproxy/templates/haproxy_config/rate_limit_spec.rb +++ b/spec/haproxy/templates/haproxy_config/rate_limit_spec.rb @@ -93,10 +93,31 @@ temp_properties.deep_merge({ 'connections_rate_limit' => { 'connections' => '5', 'block' => 'true' } }) end - it 'adds http-request deny condition to http-in and https-in frontends' do - expect(frontend_http).to include('tcp-request connection reject if { sc_conn_rate(0) gt 5 }') + it 'adds tcp-request connection reject condition to http-in and https-in frontends' do + expect(frontend_http).to include('tcp-request connection reject if { sc_conn_rate(0) gt 5 } !cidr_list_to_exclude') + expect(frontend_http).to include('tcp-request connection track-sc0 src table st_tcp_conn_rate') + expect(frontend_https).to include('tcp-request connection reject if { sc_conn_rate(0) gt 5 } !cidr_list_to_exclude') + expect(frontend_https).to include('tcp-request connection track-sc0 src table st_tcp_conn_rate') + end + end + context 'when "connections", "block" and "cidrs_to_exclude" are also provided' do + let(:properties) do + temp_properties.deep_merge( + { 'connections_rate_limit' => + { + 'connections' => '5', + 'block' => 'true', + 'cidrs_to_exclude' => ['10.0.0.0/8', '3.22.12.3/32'], + } + }) + end + + it 'adds tcp-request connection reject condition to http-in and https-in frontends' do + expect(frontend_http).to include('acl cidr_list_to_exclude src -f /var/vcap/jobs/haproxy/config/cidrs_to_exclude_from_blocking.txt') + expect(frontend_http).to include('tcp-request connection reject if { sc_conn_rate(0) gt 5 } !cidr_list_to_exclude') expect(frontend_http).to include('tcp-request connection track-sc0 src table st_tcp_conn_rate') - expect(frontend_https).to include('tcp-request connection reject if { sc_conn_rate(0) gt 5 }') + expect(frontend_https).to include('acl cidr_list_to_exclude src -f /var/vcap/jobs/haproxy/config/cidrs_to_exclude_from_blocking.txt') + expect(frontend_https).to include('tcp-request connection reject if { sc_conn_rate(0) gt 5 } !cidr_list_to_exclude') expect(frontend_https).to include('tcp-request connection track-sc0 src table st_tcp_conn_rate') end end