Wstęp#
W świecie rosnącego natężenia ruchu automatycznego (botów) i ataków DDoS staje się niezbędne wprowadzenie inteligentnych filtrów już na warstwie L7. W tym wpisie opiszę, jak dzięki prostemu skryptowi w Lua oraz mechanizmom HAProxy zbudowałem jedną z wielu warstw ochrony backendów przed nieporządanym ruchem.
Koncepcja rozwiązania#
- Wykrywanie „sesji” per IP w oparciu o cookie\
- Każda unikalna wartość ciasteczka (
__Secure-app_session) to jedna sesja,\ - każde żądanie bez ciasteczka to tzw. „no-cookie” request.
- Każda unikalna wartość ciasteczka (
- Limit 150 sesji/IP w oknie 120 s\
- Po przekroczeniu tej wartości IP zostaje oznaczone jako podejrzane.
- Akcja po przekroczeniu\
- Przekierowanie do backendu z kolejką (queue), lub drop requestu.
Skrypt Lua: szczegóły działania#
Poniżej pełny kod skryptu, wraz z opisem:
-- Globalna tabela do przechowywania stanu per IP
local ip_unique_sessions = {}
local last_cleanup = os.time()
-- Funkcja czyszcząca – resetuje całą strukturę co 120 sekund
local function cleanup()
local now = os.time()
if now - last_cleanup >= 120 then
ip_unique_sessions = {}
last_cleanup = now
end
end
-- Escapowanie znaków specjalnych w nazwie ciasteczka
local function escape_pattern(text)
return text:gsub("([^%w])", "%%%1")
end
-- Wyciąganie wartości cookie z nagłówka
local function get_cookie_value(cookie_header, cookie_name)
if not cookie_header then
return nil
end
local pattern = escape_pattern(cookie_name) .. "=([^;]+)"
return cookie_header:match(pattern)
end
-- Główna funkcja sprawdzająca limit
function check_rate_limit(txn)
-- 1) Czyszczenie przestarzałych danych
cleanup()
-- 2) Pobranie IP i nagłówka Cookie
local client_ip = txn.f:src() or "unknown"
local cookie_hdr = txn.f:hdr("Cookie")
-- 3) Wyciągnięcie wartości ciasteczka
local cookie_val = get_cookie_value(cookie_hdr, "__Secure-app_session")
-- 4) Inicjalizacja struktury, jeśli nowe IP
if not ip_unique_sessions[client_ip] then
ip_unique_sessions[client_ip] = {
cookie = {}, -- set dla unikalnych wartości cookies
no_cookie = 0 -- licznik requestów bez ciasteczka
}
end
-- 5) Aktualizacja stanu: cookie vs no_cookie
if cookie_val then
ip_unique_sessions[client_ip].cookie[cookie_val] = true
else
ip_unique_sessions[client_ip].no_cookie = ip_unique_sessions[client_ip].no_cookie + 1
end
-- 6) Zliczanie „sesji”
local count = 0
for _ in pairs(ip_unique_sessions[client_ip].cookie) do
count = count + 1
end
count = count + ip_unique_sessions[client_ip].no_cookie
-- 7) Decyzja o przekroczeniu limitu
if count > 150 then
return "true"
else
return "false"
end
end
-- Rejestracja funkcji jako sample-fetch „rate_limit”
core.register_fetches("rate_limit", check_rate_limit)
Omówienie krok po kroku#
- Zmienne globalne\
ip_unique_sessions: pusty słownik, kluczem jest IP, wartością tabela z podkluczamicookie(set) ino_cookie(licznik).\last_cleanup: znacznik czasu ostatniego pełnego resetu.
cleanup()\- Wywoływane przy każdym sprawdzeniu – czyści cały stan, jeśli od ostatniego resetu minęło ≥120 s.\
- Zapobiega niekontrolowanemu wzrostowi zużycia pamięci.
escape_pattern(text)\- Zamienia znaki specjalne w nazwie ciasteczka na escaped (
%), bystring.matchtraktował je dosłownie. Dzięki temu nazwy z podkreśleniami, myślnikami czy kropkami są bezpieczne.
- Zamienia znaki specjalne w nazwie ciasteczka na escaped (
get_cookie_value(cookie_header, cookie_name)\- Przy braku nagłówka
Cookieod razu zwracanil.\ - Buduje wzorzec:
<nazwa>=([^;]+)i zwraca pierwszy dopasowany fragment (wartość ciasteczka).
- Przy braku nagłówka
- Inicjalizacja struktury\
- Dla każdego nowego IP tworzymy tabelę z pustym setem ciasteczek i licznikiem 0.
- Aktualizacja stanu\
- Jeśli ciasteczko istnieje: dodajemy jego wartość do setu (unikalność gwarantowana),\
- jeśli nie: inkrementujemy
no_cookie.
- Zliczanie i próg\
- Sumujemy liczbę kluczy w
cookie+ wartośćno_cookie.\ - Porównujemy z progiem (150); w razie przekroczenia zwracamy
"true".
- Sumujemy liczbę kluczy w
Konfiguracja HAProxy#
Konfiguracja haproxy może wyglądać następująco:
# Zaladowanie skryptu
lua-load /opt/implix/haproxy/ddos_rate_limit.lua
# Sprawdzenie rate_limit, z pominięciem whitelisty
http-request set-var(req.rate_limit_exceeded) lua.rate_limit if !{ src -f /etc/haproxy/whitelist.txt }
# Ustawienie ddos_rate_exceeded na true jeżeli ip przekroczyło limit
acl ddos_rate_exceeded var(req.rate_limit_exceeded) -m str true
# Kierowanie do kolejkującego backendu
use_backend fpm-queue-backend if ddos_rate_exceeded
# Można też zdropować takie połączenie
# http-request deny if ddos_rate_exceeded
W moim przypadku podejrzany ruch trafia na backend z kolejką, gdzie takie żądania czekają na obsłużenie.
Jakie mogą być inne podejścia do obsługi takich połączeń?
- deny - bezpośredni zakończenie połączenia z dowolnym kodem http,
- silent-drop - zamyka połączenie po stronie HAProxy bez żadnej odpowiedzi ani komunikatu do klienta,
- tarpid - podobne do silent dropa, z tą różnicą że połączenie jest przetrzymywane przez socket haproxy.
Podsumowanie#
HAProxy dzięki skryptom Lua otwiera przed nami niemal nieograniczone możliwości zaawansowanej konfiguracji i zarządzania ruchem. Pokazany wyżej przykład to jedynie wierzchołek góry lodowej – za pomocą Lua możesz zbudować własny, w pełni programowalny load balancer, implementować niestandardowe algorytmy routingu, walidować czy modyfikować nagłówki w locie, a nawet integrować się z zewnętrznymi systemami. To elastyczne podejście pozwala dostosować HAProxy dokładnie do potrzeb Twojej infrastruktury i łatwo rozwijać go o kolejne mechanizmy ochrony czy optymalizacji.
Jeśli temat Cię wciągnął, zobacz też jak wdrożyć CAPTCHA i Proof-of-Work w HAProxy bez Enterprise Edition.