Наглые сео боты и парсеры часто могут подпортить настроение и нагрузку сервера.
У 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