Devops's Blog

HAproxy+lua+php_3 Определяем asn на лету

Ограничение и мониторинг трафика по 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