Кастомный Bot Fight mode при помощи Cloudflare API который отсеит большинство наглых ботов и парсеров

Наглые сео боты и парсеры часто могут подпортить настроение и нагрузку сервера.

У Cloudflare есть WAF и Bot Fight Mode режим, но не всё в нем доступно к сожалению в рамках free тарифа. В частности он будет пропускать всех ботов из известных этому списку https://radar.cloudflare.com/traffic/verified-bots среди которых, как раз очень наглый amazon, semrush, ahrefs и т.п. не всегда званные «гости», которые зачастую и грузят сервак.

Да, в Enterprice тарифах этот список можно конкретно шаманить, но тарифы там кусаются, особенно если это касается пула в пару-тройку сотен сайтов, для которых каждому нужен отдельный тариф…

И можно было бы просто включить на всех доменах Bot Fight Mode, но это может навредить яндекс индексации в теории.

В результате яндекс боты будут иногда блокироваться, насколько «иногда» зависит от того самого списка и определения известных клоуду ботов:
https://developers.cloudflare.com/bots/troubleshooting/#5KX8t3C6SObnoWs5F6YOlU

Обойти это на free тарифе нельзя, там написано сначала что BFM можно обойти WAF custom правилом skip.

И идея была такая в комплексе сделать для всех доменов на сервере:

1. Security -> Bots -> bot fight mode -> on
2. Security -> WAF -> custom rules -> add rule -> skip -> (http.user_agent contains «yandex» or http.user_agent contains «google»)

Попробовал — на основе просто user_agent не обходит. WAF в данном случае нужны ip адреса этих ботов для обхода BFM https://developers.cloudflare.com/bots/troubleshooting/#what-should-i-do-if-i-am-getting-false-positives-caused-by-bot-fight-mode-bfm-or-super-bot-fight-mode-sbfm

Да и это было бы не оч хорошим решением, т.к. клоуд защищает от фейковых пс ботов, а такие правила эту защиту обнулили бы.

Более менее нормальным решением тут будет такое:

Создаём своё custom правило WAF для каждого сайта. Правило такое будет, назовем его bot-fight-custom:

JavaScript challenge => (not http.user_agent contains «Yandex» and not http.user_agent contains «Google» and not http.user_agent contains «Bing» and not http.user_agent contains «DuckDuck» and not http.user_agent contains «Alexa» and not http.user_agent contains «Yahoo» and not http.user_agent contains «MSN» and not http.user_agent contains «yandex» and not http.user_agent contains «google» and not http.user_agent contains «bing» and not http.user_agent contains «Lighthouse» and not ip.src in {123.123.123.123 111.111.111.111})

Если юзерагент не содержит одну из подстрок нужных нам ботов, то им будет JavaScript challenge — это проверка js браузера. 123.123.123.123 111.111.111.111 это дополнительно разрешенные ip — обычно это ipv4 и ipv6 самого сервера, если там используются какие то http апи запросы к самому себе, чтобы они не блокировались.

Т.е. проверку будут проходить эти норм пс боты и все остальные браузеры у которых включен javascript. Обычно javascript у ботов нет, потому что это гораздо накладнее получается парсинг.

Да, это не то решение, что возможно в Enterprice тарифах, т.к. парсеры могут запросто подменить юзерагент прикинувшись и хорошим ботом. Но это другая история. В данном случае мы отсекаем без особых ухищрений, и это уже даст очень большой минус к нагрузке сервак.

Ну и накатаем небольшой php скриптик для cli, который проверит все домены(зоны) у которых в A записи указан ip нашего сервера и их WAF правила, и если правило отсутствует, то проставит через API данное правило всему пулу сайтов целевого сервера:

<?php

$x_auth_email = '[email protected]';
$x_auth_key = 'XXXXXXXXXXXXXXXXX';
$ip = '123.123.123.123';
$account_name = '[email protected]';

function req($url, $method = 'GET', $data = '', $headers = [], $timeout = 0)
{

    $curl = curl_init();

    $arr = [
        CURLOPT_URL => $url,
        CURLOPT_HEADER => false,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => '',
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT_MS => $timeout * 1000,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_POSTFIELDS => $data,
        CURLOPT_HTTPHEADER => $headers,
    ];
    curl_setopt_array($curl, $arr);

    $response = curl_exec($curl);
    curl_close($curl);
    $json = json_decode($response);
    return $json ?: $response;
}


$page = 1;
$total_pages = 1;
$zones = [];

do {
    $response = req("https://api.cloudflare.com/client/v4/zones?page=$page&per_page=50", 'GET', '', ["X-Auth-Email: $x_auth_email", "X-Auth-Key: $x_auth_key", "Content-Type: application/json"]);
    echo "Получение данных от Cloudflare, страница $page\n";
    if ($response && $response->result_info && $response->result_info->total_pages) {
        $total_pages = $response->result_info->total_pages;
        $page++;
    } else {
        echo "Ошибка получения данных от Cloudflare\n";
    }
    if ($response && $response->result && is_array($response->result)) {
        foreach ($response->result as $item) {
            if (mb_stripos($item->account->name, $account_name) === false || $item->status != 'active') {
                continue;
            }
            $zones[] = [
                'id' => $item->id,
                'name' => $item->name
            ];
        }
    }
    sleep(5);
} while ($page <= $total_pages);

if (!$zones) {
    echo "Данные от Cloudflare не получены\n";
    exit;
}

echo "Активные зоны(" . count($zones) . ") получены\n";

echo "Начинаю фильтровать зоны для $ip\n";

$zones_filtered = [];

foreach ($zones as $key => $zone) {
    $response = req("https://api.cloudflare.com/client/v4/zones/{$zone['id']}/dns_records", 'GET', '', ["X-Auth-Email: $x_auth_email", "X-Auth-Key: $x_auth_key", "Content-Type: application/json"]);
    if ($response && $response->result && is_array($response->result)) {
        foreach ($response->result as $item) {
            if ($item->type == 'A' && $item->name == $zone['name'] && $item->content == $ip) {
                $zones_filtered[] = $zone;
                echo "Зона {$zone['name']} обнаружена для $ip\n";
                break;
            }
        }
    }
}


if (!$zones_filtered) {
    echo "Зоны для $ip от Cloudflare не получены\n";
    exit;
}

$zones_waf = [];

echo "Получено " . count($zones_filtered) . " зон для $ip, начинаю проверку WAF правил...\n";

foreach ($zones_filtered as $key => $zone) {
    $response = req("https://api.cloudflare.com/client/v4/zones/{$zone['id']}/rulesets/phases/http_request_firewall_custom/entrypoint", 'GET', '', ["X-Auth-Email: $x_auth_email", "X-Auth-Key: $x_auth_key", "Content-Type: application/json"]);
    if ($response && $response->errors && $response->errors[0] && stripos($response->errors[0]->message, 'not find entrypoint') !== false) {
        $zones_waf[] = [
            'id' => $zone['id'],
            'name' => $zone['name'],
            'ruleset_id' => 0
        ];
    } else if ($response && $response->result && $response->result->id && isset($response->result->rules) && is_array($response->result->rules)) {
        $find = false;
        foreach ($response->result->rules as $rule) {
            if ($rule->action == 'js_challenge' && $rule->description == 'bot-fight-custom') {
                $find = true;
                break;
            }
        }
        if (!$find) {
            $zones_waf[] = [
                'id' => $zone['id'],
                'name' => $zone['name'],
                'ruleset_id' => $response->result->id
            ];
        }
    }

    if ($response) {
        echo "checked: {$zone['name']}\n";
    }
}

if (!$zones_waf) {
    echo "Зоны нуждающиеся в WAF правиле не обнаружены\n";
    exit;
}

echo "Получено " . count($zones_waf) . " зон для $ip, без WAF правила, начинаю установку...\n";

foreach ($zones_waf as $zone) {
    if (!$zone['ruleset_id']) {
        $data = json_encode([
            "kind" => "zone",
            "name" => "default",
            "phase" => "http_request_firewall_custom",
            "rules" => [
                [
                    "action" => "js_challenge",
                    "description" => "bot-fight-custom",
                    "enabled" => true,
                    "expression" => "(not http.user_agent contains \"Yandex\" and not http.user_agent contains \"Google\" and not http.user_agent contains \"Bing\" and not http.user_agent contains \"DuckDuck\" and not http.user_agent contains \"Alexa\" and not http.user_agent contains \"Yahoo\" and not http.user_agent contains \"MSN\" and not http.user_agent contains \"yandex\" and not http.user_agent contains \"google\" and not http.user_agent contains \"bing\" and not http.user_agent contains \"Lighthouse\" and not ip.src in {123.123.123.123 111.111.111.111})"
                ]
            ]
        ]);
        $response = req("https://api.cloudflare.com/client/v4/zones/{$zone['id']}/rulesets", 'POST', $data, ["X-Auth-Email: $x_auth_email", "X-Auth-Key: $x_auth_key", "Content-Type: application/json"]);
        if ($response && $response->success) {
            echo "Создано новое WAF правило: {$zone['name']}\n";
        } else {
            echo "Ошибка установки WAF правила: {$zone['name']}\n";
        }
    } else {
        $data = json_encode([
            "action" => "js_challenge",
            "description" => "bot-fight-custom",
            "enabled" => true,
            "expression" => "(not http.user_agent contains \"Yandex\" and not http.user_agent contains \"Google\" and not http.user_agent contains \"Bing\" and not http.user_agent contains \"DuckDuck\" and not http.user_agent contains \"Alexa\" and not http.user_agent contains \"Yahoo\" and not http.user_agent contains \"MSN\" and not http.user_agent contains \"yandex\" and not http.user_agent contains \"google\" and not http.user_agent contains \"bing\" and not http.user_agent contains \"Lighthouse\" and not ip.src in {123.123.123.123 111.111.111.111})"
        ]);
        $response = req("https://api.cloudflare.com/client/v4/zones/{$zone['id']}/rulesets/{$zone['ruleset_id']}/rules", 'POST', $data, ["X-Auth-Email: $x_auth_email", "X-Auth-Key: $x_auth_key", "Content-Type: application/json"]);
        if ($response && $response->success) {
            echo "Создано новое WAF правило: {$zone['name']}\n";
        } else {
            echo "Ошибка установки WAF правила: {$zone['name']}\n";
        }
    }
}

ну а далее эту задачку можно поставить и на cron раз в недельку, чтобы она добавляла правило новым доменам

0 0 * * 0 root (php -f /path/cloud-bot-fight.php &) > /dev/null 2>&1

Оставить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *