Ограничение и мониторинг трафика по ASN в HAProxy
-
Для каждого HTTP-запроса с заголовком
x-deviceопределяется ASN (Autonomous System Number) по IP-адресу клиента. Это делается Lua-скриптом, который обращается к отдельному backend-сервису (php-скрипт через FastCGI) с использованием базы GeoLite2-ASN. - Полученный ASN, а также версия приложения, локаль и география объединяются в специальный заголовок xbots4.
-
Для уникальных значений
xbots4ведется stick-table, в которой фиксируется частота запросов по разным путям (например,/search,/conver) за 10 секунд. -
Запросы отклоняются с ошибкой 429, если превышены лимиты (30 на
/searchили 50 на/converза 10 секунд). - После обработки технический заголовок xbots4 удаляется.
- Stick-table синхронизируется между несколькими HAProxy-инстансами через peers traffic_ext4.
- Вся логика определения ASN выполняется отдельным php-скриптом (asn.php), который использует библиотеку MaxMind для поиска ASN по IP.
- Для обращения к сервису ASN выделен отдельный frontend и backend в HAProxy.
Назначение: детектировать и ограничивать активность аномальных или массовых запросов по ASN (автономным системам), а также собирать статистику в разрезе ASN, версии приложения и географии для последующего анализа и борьбы с фродом.
lua-load asn_lookup.lua
peers traffic_ext4
bind *:10004
server localpeers
backend bruteforce_ext_1h_4
stick-table type string len 200 size 1m expire 10s store gpc0,gpc1,gpc0_rate(10s),gpc1_rate(10s),http_req_rate(10s) peers traffic_ext4
http-request lua.set_asn_var if { req.hdr(x-device) -m found }
http-request set-header xbots4 %[var(txn.asn)]_%[req.fhdr(x-application-version)]_%[req.fhdr(x-locale)]_%[req.fhdr(ip-geo)] if { var(txn.asn) -m reg .+ -m reg -v '^unknown$' }
http-request track-sc3 hdr(xbots4) table bruteforce_ext_1h_4 if { var(txn.asn) -m reg .+ -m reg -v '^unknown$' }
http-request sc-inc-gpc0(3) if { path -i /search }
http-request sc-inc-gpc1(3) if { path -i /conver }
http-request deny deny_status 429 if ext_ip { sc_get_gpc0(3,bruteforce_ext_1h_4) ge 30 } { path -i /search }
http-request deny deny_status 429 if ext_ip { sc_get_gpc1(3,bruteforce_ext_1h_4) ge 50 } { path -i /conver }
http-request del-header xbots4 if ext_ip
frontend haproxy_asn
bind *:8887
mode http
default_backend bknd_haproxy_asn
backend bknd_haproxy_asn
balance source
option allbackups
timeout server 200ms
option http-server-close
use-fcgi-app php-fpm_asn
server php-fpm 127.0.0.1:4141 proto fcgi check port 4141 inter 3000
fcgi-app php-fpm_asn
log-stderr global
docroot /opt/web/asn/
index asn.php
core.register_action("set_asn_var", { "http-req" }, function(txn)
local ip = txn.sf:req_hdr("x-real-ip") or "empty"
local backend_ip = "127.0.0.1"
local backend_port = 8887
local path = "/asn.php?ip=" .. ip
local request_str =
"GET " .. path .. " HTTP/1.1\r\n" ..
"Host: localhost\r\n" ..
"Connection: close\r\n\r\n"
local sock, err = core.tcp()
if not sock then
core.Debug("[set_asn_var] TCP socket error: " .. tostring(err))
txn:set_var("txn.asn", "unknown")
return
end
if not sock:connect(backend_ip, backend_port, 500) then
core.Debug("[set_asn_var] TCP connect error")
txn:set_var("txn.asn", "unknown")
sock:close()
return
end
sock:send(request_str)
local status_line = sock:receive("*l")
if not status_line then
core.Debug("[set_asn_var] Empty status line")
txn:set_var("txn.asn", "unknown")
sock:close()
return
end
while true do
local line = sock:receive("*l")
if not line or line == "" then break end
end
local body = sock:receive("*a")
sock:close()
--core.Debug("[asn_debug] : '" .. body .. "'")
local asn = "unknown"
if body then
local m = body:match('"asn"%s*:%s*("?%d+"?)')
if m then
asn = m:gsub('"','')
end
end
txn:set_var("txn.asn", asn)
end)
require '/opt/web/vendor/autoload.php';
use GeoIp2\Database\Reader;
header('Content-Type: application/json');
if (!isset($_GET['ip'])) {
echo json_encode(['asn' => 'unknown']);
exit;
}
$ip = $_GET['ip'];
try {
$reader = new Reader('GeoLite2-ASN.mmdb');
$asn = $reader->asn($ip);
echo json_encode([
'asn' => $asn->autonomousSystemNumber ?? 'unknown'
]);
} catch (Exception $e) {
echo json_encode(['asn' => 'unknown']);
}
Или можно php скрипт можно запустить как сервис и в lua меняем 8887->6661 , тогда не сможем мониторить обращение к нему
[root@srv gzt]# cat /usr/lib/systemd/system/asn.service
[Unit]
Description=asn
[Service]
ExecStart=/usr/bin/php -S 127.0.0.1:6661 /opt/web/asn/asn.php
[Install]
WantedBy=multi-user.target
[root@srv haproxy]# echo "show table bruteforce_ext_1h_4" | socat stdio /run/haproxy.stat
# table: bruteforce_ext_1h_4, type: string, size:1048576, used:3
0x7fb2619aaf00: key=4837_5.5.3_en_CN use=0 exp=8983 shard=0 gpc0=0 gpc0_rate(10000)=0 http_req_rate(10000)=3 gpc1=3 gpc1_rate(10000)=3
0x7fb23dda8970: key=8359_5.5.1_en_RU use=0 exp=8775 shard=0 gpc0=0 gpc0_rate(10000)=0 http_req_rate(10000)=1 gpc1=1 gpc1_rate(10000)=1
0x7fb265716f30: key=24445_5.5.3_en_CN use=0 exp=7309 shard=0 gpc0=0 gpc0_rate(10000)=0 http_req_rate(10000)=1 gpc1=1 gpc1_rate(10000)=1