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. - 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.match
traktował je dosłownie. Dzięki temu nazwy z podkreśleniami, myślnikami czy kropkami są bezpieczne.get_cookie_value(cookie_header, cookie_name)
- Przy braku nagłówkaCookie
od razu zwracanil
.
- Buduje wzorzec:<nazwa>=([^;]+)
i zwraca pierwszy dopasowany fragment (wartość ciasteczka).- 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: inkrementujemyno_cookie
. - Zliczanie i próg
- Sumujemy liczbę kluczy wcookie
+ wartośćno_cookie
.
- Porównujemy z progiem (150); w razie przekroczenia zwracamy"true"
.
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.