Devops's Blog

HAproxy+lua_2 Учет уникальных устройств и IP-адресов в HAProxy для мониторинга и борьбы с фродом

Скрипт-счетчик уникальных устройств и IP для HAProxy с учетом geo и доменов
  • Считаем количество уникальных устройств (x-device) и уникальных пар IP+география (ip-geo) внутри окна 10 минут.
  • Работаем только для разрешённых доменов: api1.rts.ru, api1.tld.ru, api1.center.ru, service1.lines.com. Для других — счетчики не увеличиваются.
  • Исключаем некоторые эндпоинты и хосты (webhooks, /3.0/fb/api/, hotels, /3.0/voice, rst.api1.ff.ru).
  • Разделяем обработку на два режима: обычный и “fly” для определённых стран (например, RU, CN, KZ и др. — см. ignore_countries).
  • В “fly” режиме отдельное хранение данных и счетчики.
  • Для каждой пары (IP+geo) ведёт ассоциативный массив устройств с меткой времени и локалью; для каждого устройства — аналогичным образом ассоциативный массив IP+geo.
  • Если количество уникальных ключей в одной таблице превышает 25 000, таблица для этого сегмента очищается.
  • Для каждого сегмента считает: всего уникальных устройств, устройств с ru-локалью, устройств с не-ru-локалью; аналогично для уникальных IP.
  • Все счетчики пишутся в переменные HAProxy txn:set_var для дальнейшей обработки или логирования.
  • При нехватке обязательных данных (ip, geo, x-device) — все счетчики = 0.

Назначение: собирать в разрезе домена, географии и локали информацию о количестве уникальных устройств и IP за последние 10 минут, разделяя обычные и “fly” страны, для целей мониторинга, анализа аномалий и противодействия фроду.


lua-load       count_uniq_xdevice_and_ip.lua
#count_uniq_x-device_ip
    http-request lua.count_uniq_xdevice_and_ip if { req.hdr(x-device) -m found }
 

local ttl = 600
local MAX_KEYS_PER_TABLE = 25000
local ignore_countries = {
    CN=true, TH=true, AE=true, KG=true, KZ=true,
    UZ=true, TJ=true, AM=true, TR=true, TM=true,
    AZ=true, RU=true, BY=true
}

local allowed_domains = {
    ["api1.rts.ru"]=true,
    ["api1.tld.ru"]=true,
    ["api1.center.ru"]=true,
    ["service1.lines.com"]=true
}

local function is_non_empty(val)
    return val and val:match("^%s*$") == nil
end

local function is_allowed_host(host)
    if not host then return false end
    host = host:lower()
    for dom in pairs(allowed_domains) do
        if host:sub(-#dom) == dom then
            return true
        end
    end
    return false
end

local function is_excluded(host, path)
    if not host then host = "" end
    if not path then path = "" end
    host, path = host:lower(), path:lower()
    return (
        path:find("webhooks", 1, true) or
        path:sub(1, #"/3.0/fb/api/") == "/3.0/fb/api/" or
        path:find("hotels", 1, true) or
        path:sub(1, #"/3.0/voice") == "/3.0/voice" or
        host == "rst.api1.ff.ru"
    )
end

core.register_init(function()
    devices_by_ipgeo = {}
    ips_by_device = {}
    fly_devices_by_ipgeo = {}
    fly_ips_by_device = {}
end)

local function table_size(tbl)
    local count = 0
    for _ in pairs(tbl) do count = count + 1 end
    return count
end

core.register_action("count_uniq_xdevice_and_ip", {"http-req"}, function(txn)
    local xff    = txn.sf:req_hdr("X-Real-IP")
    local ip     = txn.sf:req_hdr("X-Real-IP")
    local geo    = (txn.sf:req_hdr("ip-geo") or ""):upper()
    local device = txn.sf:req_hdr("x-device")
    local host   = txn.sf:req_hdr("host")
    local path   = txn.sf:path()
    local xlocale = txn.sf:req_hdr("x-locale") or ""
    local xlocale_lower = xlocale:lower()
    local ipgeo  = ip .. "_" .. geo
    local now    = os.time()
    txn:set_var("req.xff_chain", xff or "")

    local all_vars = {
        "uniq_xdevice_count", "uniq_ip_count", "uniq_xdevice_count_fly", "uniq_ip_count_fly",
        "uniq_xdevice_count_not_lan_ru", "uniq_ip_count_not_lan_ru",
        "uniq_xdevice_count_fly_not_lan_ru", "uniq_ip_count_fly_not_lan_ru",
        "uniq_xdevice_count_lan_ru", "uniq_ip_count_lan_ru",
        "uniq_xdevice_count_fly_lan_ru", "uniq_ip_count_fly_lan_ru",
        "dev_table_size", "ip_table_size"
    }

    if not is_non_empty(ip) or not is_non_empty(geo) or not is_non_empty(device) then
        for _,v in ipairs(all_vars) do txn:set_var("req."..v, 0) end
        return
    end
    if not is_allowed_host(host) then
        for _,v in ipairs(all_vars) do txn:set_var("req."..v, 0) end
        return
    end
    if is_excluded(host, path) then
        for _,v in ipairs(all_vars) do txn:set_var("req."..v, 0) end
        return
    end

    local mode = ignore_countries[geo] and "fly" or "common"

    -- --- devices_by_ipgeo ---
    local devtbl
    if mode == "fly" then
        fly_devices_by_ipgeo[ipgeo] = fly_devices_by_ipgeo[ipgeo] or {}
        devtbl = fly_devices_by_ipgeo[ipgeo]
    else
        devices_by_ipgeo[ipgeo] = devices_by_ipgeo[ipgeo] or {}
        devtbl = devices_by_ipgeo[ipgeo]
    end

    for d, info in pairs(devtbl) do
        local ts = (type(info)=="table" and info[1]) or info
        if now - ts > ttl then
            devtbl[d] = nil
        end
    end
    if table_size(devtbl) >= MAX_KEYS_PER_TABLE then
        devtbl = {}
        if mode == "fly" then fly_devices_by_ipgeo[ipgeo] = devtbl else devices_by_ipgeo[ipgeo] = devtbl end
    end
    devtbl[device] = {now, xlocale_lower}
    txn:set_var("req.dev_table_size", table_size(devtbl))

    -- --- ips_by_device ---
    local iptbl
    if mode == "fly" then
        fly_ips_by_device[device] = fly_ips_by_device[device] or {}
        iptbl = fly_ips_by_device[device]
    else
        ips_by_device[device] = ips_by_device[device] or {}
        iptbl = ips_by_device[device]
    end

    for ipg, info in pairs(iptbl) do
        local ts = (type(info)=="table" and info[1]) or info
        if now - ts > ttl then
            iptbl[ipg] = nil
        end
    end
    if table_size(iptbl) >= MAX_KEYS_PER_TABLE then
        iptbl = {}
        if mode == "fly" then fly_ips_by_device[device] = iptbl else ips_by_device[device] = iptbl end
    end
    iptbl[ipgeo] = {now, xlocale_lower}
    txn:set_var("req.ip_table_size", table_size(iptbl))

    -- ---- UNIQUE COUNTS: device ----
    local count_all = 0
    local count_lan_ru = 0
    local count_not_lan_ru = 0
    for _, info in pairs(devtbl) do
        local ts, loc = info[1], info[2]
        if now - ts <= ttl then
            count_all = count_all + 1
            if loc == "ru" then count_lan_ru = count_lan_ru + 1
            elseif loc ~= "" then count_not_lan_ru = count_not_lan_ru + 1 end
        end
    end

    -- ---- UNIQUE COUNTS: ip ----
    local countip_all = 0
    local countip_lan_ru = 0
    local countip_not_lan_ru = 0
    for _, info in pairs(iptbl) do
        local ts, loc = info[1], info[2]
        if now - ts <= ttl then
            countip_all = countip_all + 1
            if loc == "ru" then countip_lan_ru = countip_lan_ru + 1
            elseif loc ~= "" then countip_not_lan_ru = countip_not_lan_ru + 1 end
        end
    end

    if mode == "fly" then
        txn:set_var("req.uniq_xdevice_count_fly", count_all)
        txn:set_var("req.uniq_xdevice_count_fly_lan_ru", count_lan_ru)
        txn:set_var("req.uniq_xdevice_count_fly_not_lan_ru", count_not_lan_ru)
        txn:set_var("req.uniq_ip_count_fly", countip_all)
        txn:set_var("req.uniq_ip_count_fly_lan_ru", countip_lan_ru)
        txn:set_var("req.uniq_ip_count_fly_not_lan_ru", countip_not_lan_ru)

        txn:set_var("req.uniq_xdevice_count", 0)
        txn:set_var("req.uniq_xdevice_count_lan_ru", 0)
        txn:set_var("req.uniq_xdevice_count_not_lan_ru", 0)
        txn:set_var("req.uniq_ip_count", 0)
        txn:set_var("req.uniq_ip_count_lan_ru", 0)
        txn:set_var("req.uniq_ip_count_not_lan_ru", 0)
    else
        txn:set_var("req.uniq_xdevice_count", count_all)
        txn:set_var("req.uniq_xdevice_count_lan_ru", count_lan_ru)
        txn:set_var("req.uniq_xdevice_count_not_lan_ru", count_not_lan_ru)
        txn:set_var("req.uniq_ip_count", countip_all)
        txn:set_var("req.uniq_ip_count_lan_ru", countip_lan_ru)
        txn:set_var("req.uniq_ip_count_not_lan_ru", countip_not_lan_ru)

        txn:set_var("req.uniq_xdevice_count_fly", 0)
        txn:set_var("req.uniq_xdevice_count_fly_lan_ru", 0)
        txn:set_var("req.uniq_xdevice_count_fly_not_lan_ru", 0)
        txn:set_var("req.uniq_ip_count_fly", 0)
        txn:set_var("req.uniq_ip_count_fly_lan_ru", 0)
        txn:set_var("req.uniq_ip_count_fly_not_lan_ru", 0)
    end
end)