
¡Buen día! En este artículo, explicaré cómo los usuarios de hosting compartido pueden capturar las direcciones IP que generan una carga excesiva en el sitio y luego bloquearlas utilizando las herramientas del hosting; habrá un poco de código PHP y algunas capturas de pantalla.
Datos de entrada:
- Sitio web creado en CMS WordPress
- Hosting Beget (no es un anuncio, pero las capturas de pantalla del panel de administración serán de este proveedor de hosting)
- El sitio WordPress se lanzó a principios de 2000 y tiene una gran cantidad de artículos y materiales
- Versión de PHP 7.2
- WP tiene la versión más reciente
- Desde hace algún tiempo, el sitio ha comenzado a generar una alta carga en MySQL según los datos del hosting. Cada día este valor superaba el 120% de la norma para la cuenta
- Según Yandex.Metrica, el sitio recibe de 100 a 200 visitantes al día
En primer lugar, se realizó lo siguiente:
- Se limpiaron las tablas de la base de datos del basurero acumulado
- Se desactivaron plugins innecesarios y se eliminaron secciones de código obsoleto
Al mismo tiempo, hago hincapié en que se probaron opciones de caché (plugins de caché), se realizaron observaciones, pero la carga del 120% de un solo sitio se mantuvo constante y solo pudo aumentar.
Así es como se veía la carga aproximada en las bases de datos del hosting

En la parte superior está el sitio del que se habla, un poco más abajo otros sitios que tienen el mismo CMS y aproximadamente el mismo nivel de visitas, pero generan menos carga.
Análisis
- Se hicieron muchos intentos con opciones de caché, y se realizaron observaciones durante varias semanas (afortunadamente, durante este tiempo el hosting no me escribió una sola vez que era un mal usuario y que me iban a desconectar)
- Se realizó un análisis y búsqueda de consultas lentas, luego se modificó un poco la estructura de la base de datos y el tipo de tablas
- Para el análisis, se utilizó principalmente AWStats integrado (que, por cierto, ayudó a detectar la dirección IP más problemática por volumen de tráfico
- Estadísticas: la métrica solo da información sobre personas, no sobre bots
- Se realizaron intentos de utilizar plugins para WP que pueden filtrar y bloquear visitantes incluso por país de origen y por varias combinaciones
- Una manera radical de cerrar el sitio por un día con la nota 'Estamos en mantenimiento' fue mediante el famoso plugin. En este caso, se esperaba que la carga disminuyera, pero no a cero, ya que la ideología de WP se basa en hooks y los plugins comienzan su actividad al producirse algún 'hook', y antes de que ocurra ese 'hook' ya pueden haberse realizado consultas a la base de datos.
La idea
- Calcular las direcciones IP que realizan muchas consultas en un corto período de tiempo.
- Registrar el número de accesos al sitio
- Bloquear el acceso al sitio basado en el número de accesos
- Bloquear mediante la entrada 'Deny from' en el archivo .htaccess
- Otras opciones, como iptables y reglas para Nginx, no se consideran, ya que estoy escribiendo sobre hosting.
Si surge una idea, entonces hay que implementarla, ¿cómo sería sin ello?
- Creamos tablas para acumular datos
CREATE TABLE `wp_visiters_bot` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `ip` VARCHAR(300) NULL DEFAULT NULL, `browser` VARCHAR(500) NULL DEFAULT NULL, `cnt` INT(11) NULL DEFAULT NULL, `request` TEXT NULL, `input` TEXT NULL, `data_update` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE INDEX `ip` (`ip`) ) COMMENT='Candidatos para bloqueo' COLLATE='utf8_general_ci' ENGINE=InnoDB AUTO_INCREMENT=1;CREATE TABLE `wp_visiters_bot_blocked` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `ip` VARCHAR(300) NOT NULL, `data_update` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE INDEX `ip` (`ip`) ) COMMENT='Lista ya bloqueada' COLLATE='utf8_general_ci' ENGINE=InnoDB AUTO_INCREMENT=59;CREATE TABLE `wp_visiters_bot_history` ( `id` INT(11) NOT NULL AUTO_INCREMENT, `ip` VARCHAR(300) NULL DEFAULT NULL, `browser` VARCHAR(500) NULL DEFAULT NULL, `cnt` INT(11) NULL DEFAULT NULL, `data_update` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `data_add` DATETIME NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE INDEX `ip` (`ip`) ) COMMENT='Historia de todas las solicitudes para depuración' COLLATE='utf8_general_ci' ENGINE=InnoDB AUTO_INCREMENT=1; - Crearemos un archivo en el que colocaremos el código. El código hará entradas en las tablas de candidatos para el bloqueo y mantendrá un historial para la depuración.
Código del archivo para registrar direcciones IP
<?php if (!defined('ABSPATH')) { return; } global $wpdb; /** * Вернёт конкретный IP адрес посетителя * @return boolean */ function coderun_get_user_ip() { $client_ip = ''; $address_headers = array( 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR', ); foreach ($address_headers as $header) { if (array_key_exists($header, $_SERVER)) { $address_chain = explode(',', $_SERVER[$header]); $client_ip = trim($address_chain[0]); break; } } if (!$client_ip) { return ''; } if ('0.0.0.0' === $client_ip || '::' === $client_ip || $client_ip == 'unknown') { return ''; } return $client_ip; } $ip = esc_sql(coderun_get_user_ip()); // IP адрес посетителя if (empty($ip)) {// Нет IP, ну и идите лесом... header('Content-type: application/json;'); die('Big big bolt....'); } $browser = esc_sql($_SERVER['HTTP_USER_AGENT']); //Данные для анализа браузера $request = esc_sql(wp_json_encode($_REQUEST)); //Последний запрос который был к сайту $input = esc_sql(file_get_contents('php://input')); //Тело запроса, если было $cnt = 1; //Запрос в основную таблицу с временными кондидатами на блокировку $query = <<<EOT INSERT INTO wp_visiters_bot (`ip`,`browser`,`cnt`,`request`,`input`) VALUES ('{$ip}','{$browser}','{$cnt}','{$request}','$input') ON DUPLICATE KEY UPDATE cnt=cnt+1,request=VALUES(request),input=VALUES(input),browser=VALUES(browser) EOT; //Запрос для истории $query2 = <<<EOT INSERT INTO wp_visiters_bot_history (`ip`,`browser`,`cnt`) VALUES ('{$ip}','{$browser}','{$cnt}') ON DUPLICATE KEY UPDATE cnt=cnt+1,browser=VALUES(browser) EOT; $wpdb->query($query); $wpdb->query($query2);La esencia del código es obtener la dirección IP del visitante y registrarla en la tabla. Si la IP ya está en la tabla, se incrementará el campo cnt (número de solicitudes al sitio).
- Ahora lo terrible… Ahora me quemarán por mis acciones 🙂
Para registrar cada acceso al sitio, conectamos el código del archivo en el archivo principal de WordPress — wp-load.php. Sí, estamos modificando el archivo del núcleo y solo después de que ya exista la variable global $wpdb.
Entonces, ahora podemos ver con qué frecuencia se marca una dirección IP en nuestra tabla y, con una taza de café, echamos un vistazo cada 5 minutos para entender la situación.

A continuación, simplemente copiamos la IP "maliciosa", abrimos el archivo .htaccess y la añadimos al final del archivo.
Order allow,deny
Allow from all
# start_auto_deny_list
Deny from 94.242.55.248
# end_auto_deny_list
Eso es, ahora 94.242.55.248 no tiene acceso al sitio y no genera carga en la base de datos.
Pero copiar manualmente cada vez no es una tarea muy correcta, además, el código se pensó para ser autónomo.
Añadiremos un archivo que se ejecutará por CRON cada 30 minutos:
Código del archivo que modifica .htaccess
<?php
/**
* Archivo de tarea automática de bloqueos por dirección IP
* Debe ser solicitado a través de CRON
*\/
if (empty($_REQUEST['key'])) {
die('Hello');
}
require('wp-load.php');
global $wpdb;
$limit_cnt = 70; // Límite de solicitudes a considerar
$deny_table = $wpdb->get_results("SELECT * FROM wp_visiters_bot WHERE cnt>{$limit_cnt}");
$new_blocked = [];
$exclude_ip = [
'87.236.16.70' // dirección del hosting
];
foreach ($deny_table as $result) {
if (in_array($result->ip, $exclude_ip)) {
continue;
}
$wpdb->insert('wp_visiters_bot_blocked', ['ip' => $result->ip], ['%s']);
}
$deny_table_blocked = $wpdb->get_results("SELECT * FROM wp_visiters_bot_blocked");
foreach ($deny_table_blocked as $blocked) {
$new_blocked[] = $blocked->ip;
}
// Limpieza de la tabla
$wpdb->query("DELETE FROM wp_visiters_bot");
//echo '<pre>';print_r($new_blocked);echo '<\/pre>';
$file = '.htaccess';
$start_searche_tag = 'start_auto_deny_list';
$end_searche_tag = 'end_auto_deny_list';
$handle = @fopen($file, "r");
if ($handle) {
$replace_string = ''; // Test para insertar en el archivo .htaccess
$target_content = false; // Bandera para el segmento de código requerido
while (($buffer = fgets($handle, 4096)) !== false) {
if (stripos($buffer, 'start_auto_deny_list') !== false) {
$target_content = true;
continue;
}
if (stripos($buffer, 'end_auto_deny_list') !== false) {
$target_content = false;
continue;
}
if ($target_content) {
$replace_string .= $buffer;
}
}
if (!feof($handle)) {
echo "Error: fgets() falló inesperadamente\n";
}
fclose($handle);
}
// Archivo actual .htaccess
$content = file_get_contents($file);
$content = str_replace($replace_string, '', $content);
// Limpiamos todos los bloqueos en el archivo .htaccess
file_put_contents($file, $content);
// Escribimos nuevos bloqueos
$str = "# {$start_searche_tag}" . PHP_EOL;
foreach ($new_blocked as $key => $value) {
$str .= "Deny from {$value}" . PHP_EOL;
}
file_put_contents($file, str_replace("# {$start_searche_tag}", $str, file_get_contents($file)));
El código del archivo es bastante simple y primitivo, y su idea principal es tomar candidatos para bloqueo y escribir las reglas de bloqueo en el archivo .htaccess entre los comentarios.
# start_auto_deny_list и # end_auto_deny_list
Ahora las IP "maliciosas" se bloquean por sí mismas, y el archivo .htaccess se ve aproximadamente así:
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
Order allow,deny
Allow from all
# start_auto_deny_list
Deny from 94.242.55.248
Deny from 207.46.13.122
Deny from 66.249.64.164
Deny from 54.209.162.70
Deny from 40.77.167.86
Deny from 54.146.43.69
Deny from 207.46.13.168
....... ниже другие адреса
# end_auto_deny_list
Como resultado, después de que este código comience a funcionar, se puede ver el resultado en el panel de hosting:

PD: El material es original, aunque una parte la publiqué en mi sitio, en Habre obtuve una versión más amplia.
Fuente: habr.com
