zoolok / laravel-ip-blocker
Laravel package for blocking suspicious IP addresses (anti-spam/anti-scanner). Supports nginx and Apache log parsing.
Requires
- php: ^8.1
- ext-pcre: *
- illuminate/console: ^10.0|^11.0|^12.0|^13.0
- illuminate/database: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/mail: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0
Suggests
- laravel/framework: Required for full Laravel integration.
- moonshine/moonshine: Optional MoonShine admin panel resource for viewing blocked IPs.
README
Блокировка подозрительных IP-адресов для Laravel. Поддерживает nginx и Apache.
Возможности
- ✅ Middleware — отслеживает подозрительные ответы, проверяет блокировку, возвращает 403
- ✅ Парсинг логов — читает access.log (nginx combined, Apache common/combined)
- ✅ Автоопределение формата — определяет nginx или Apache по первой строке файла
- ✅ Обнаружение сканеров — по User-Agent и путям даже при ответе 200 (ExchangeScanner, zgrab и т.п.)
- ✅ Автоматическая блокировка — анализирует подозрительную активность и блокирует IP
- ✅ Генерация deny-конфига — создаёт nginx deny или Apache Require not ip конфигурацию
- ✅ Ежедневный отчёт — email-рассылка со статистикой блокировок
- ✅ Автоочистка подозрительных запросов — удаляет записи
suspicious_requestsдля активных блокировок по планировщику (вкл/выкл + cron) - ✅ MoonShine 3.x / 4.x — опциональный админ-ресурс для просмотра блокировок
- ✅ Инкрементальный парсинг — запоминает позицию в логе для последующих проходов
Требования
- PHP ^8.1
- Laravel ^10.0|^11.0|^12.0|^13.0
- MoonShine ^3.0|^4.0 (только для интеграции с админ-панелью)
- nginx или Apache (для генерации deny-конфигов)
Установка
composer require zoolok/laravel-ip-blocker
Публикация конфига и миграций (опционально):
php artisan vendor:publish --tag=ip-blocker-config php artisan vendor:publish --tag=ip-blocker-migrations php artisan migrate
Быстрый старт
1. Подключите Middleware
В bootstrap/app.php (Laravel 11+) или Http/Kernel.php:
// Глобально — на все запросы ->withMiddleware(function (Middleware $middleware) { $middleware->append(\Zoolok\IpBlocker\Http\Middleware\TrackSuspiciousIps::class); }) // Или на группу роутов Route::middleware('suspicious-ip')->group(function () { // ... });
2. Настройте конфиг
IP_BLOCKER_LOG_PATH=/var/log/nginx/access.log IP_BLOCKER_LOG_FORMAT=auto IP_BLOCKER_SERVER_TYPE=nginx IP_BLOCKER_DENY_PATH=/etc/nginx/conf.d/blocked-ips.conf IP_BLOCKER_RELOAD_CMD="nginx -s reload"
3. Запустите парсинг логов (опционально)
Middleware фиксирует только новые 4xx-запросы с момента установки, поэтому база suspicious_requests изначально пуста. Чтобы сразу анализировать историю, заполните её из существующего access.log:
php artisan ip:parse-log
Опции:
--path— путь к лог-файлу (переопределяет конфиг)--format— формат (auto, nginx-combined, apache-common, apache-combined)--dry-run— только вывод, без сохранения в БД--from-beginning— парсить с начала файла (игнорировать сохранённую позицию)--block— после парсинга сразу запуститьip:block(удобно для cron)
4. Заблокируйте подозрительные IP
php artisan ip:block
Опции:
--ip— заблокировать конкретный IP--reason— причина блокировки--dry-run— показать, кто будет заблокирован, без блокировки--force— пропустить подтверждение--no-nginx— не генерировать deny-конфиг
Разблокировка IP
Команда ip:unblock снимает блокировку полностью за один шаг: удаляет IP из
таблицы blocked_ips, удаляет его записи из suspicious_requests (иначе
парсер лога снова его заблокирует) и перегенерирует deny-конфиг веб-сервера,
чтобы nginx/Apache перестали возвращать 403.
php artisan ip:unblock --ip 89.207.69.111
Опции:
--all— разблокировать все IP и очистить все подозрительные запросы--force— пропустить подтверждение--no-nginx— не перегенерировать deny-конфиг
Автоматизация (cron / планировщик)
Для автоматической защиты по логам используйте связку ip:parse-log --block:
*/5 * * * * php /path/to/artisan ip:parse-log --block --no-interaction
Пакет запоминает позицию в логе, поэтому каждый запуск обрабатывает только новые строки, а затем --block анализирует и блокирует нарушителей. Middleware при этом продолжает работать независимо: он фиксирует запросы в реальном времени и возвращает 403 уже заблокированным IP, но сам блокировки не создаёт — за это отвечает только ip:block. Записи одного и того же запроса могут попасть в suspicious_requests дважды (через middleware и через парсинг лога), это не мешает блокировке.
Встроенный планировщик
Пакет может сам регистрировать задачу ip:parse-log --block в Laravel-планировщике — отдельный cron не нужен. Достаточно включить в конфиге (или .env):
IP_BLOCKER_SCHEDULER_ENABLED=true
# IP_BLOCKER_SCHEDULER_SCHEDULE=*/5 * * * * # по умолчанию каждые 5 минут
При этом у Laravel должен быть запущен планировщик:
* * * * * php /path/to/artisan schedule:run
Задача использует withoutOverlapping(), чтобы параллельные запуски не пересекались.
Очистка подозрительных запросов для заблокированных IP
Команда ip:cleanup-suspicious удаляет из таблицы suspicious_requests все записи,
принадлежащие активным блокировкам (is_active = true, срок не истёк). Это
позволяет не копить историю сканирования для IP, которые уже заблокированы и больше
не могут быть заблокированы повторно. Записи для истёкших или удалённых блокировок
остаются нетронутыми.
# Посмотреть, что будет удалено (без удаления) php artisan ip:cleanup-suspicious --dry-run # Удалить записи для всех активных блокировок php artisan ip:cleanup-suspicious
Автоматический запуск раз в день через Laravel-планировщик управляется конфигом:
IP_BLOCKER_CLEANUP_SUSPICIOUS_ENABLED=true
# IP_BLOCKER_CLEANUP_SUSPICIOUS_SCHEDULE=0 3 * * * # по умолчанию каждый день в 3:00
При этом у Laravel должен быть запущен планировщик (см. выше). Задача использует
withoutOverlapping(), чтобы параллельные запуски не пересекались.
Интеграция с MoonShine
Пакет предоставляет готовый ресурс для админ-панели MoonShine для просмотра заблокированных IP. Совместим с MoonShine 3.x и 4.x. Ресурс автоматически регистрируется, когда включён в конфиге:
IP_BLOCKER_MOONSHINE_ENABLED=true
Если в вашем приложении меню админ-панели формируется вручную (переопределён метод menu() в лейауте), добавьте пункт меню автоматически:
php artisan ip:install-moonshine
Команда сама найдёт активный лейаут MoonShine (по конфигу moonshine.layout), добавит use-импорт ресурса и пункт меню «Заблокированные IP». Команда идемпотентна — повторный запуск ничего не дублирует. Для принудительной перевставки используйте --force.
На индексной странице ресурса есть быстрые фильтры «Активные» и «Истекшие».
«Истекшие» считаются по сроку истечения (expires_at <= now()) или по флагу
is_active = false, а не только по флагу: пакет никогда не переводит
is_active в false автоматически, поэтому истёкшие записи остаются с
is_active = true. С v1.6.6 фильтры корректно разделяют действующие и
истёкшие блокировки.
Создание новых записей из админки отключено (они создаются командой
ip:block), но каждая строка доступна для редактирования и удаления.
После сохранения или удаления записи deny-конфиг веб-сервера автоматически
перегенерируется из таблицы blocked_ips (см. IP_BLOCKER_SYNC_ON_CHANGE).
Поддерживаемые форматы логов
| Формат | Конфиг | Пример |
|---|---|---|
| nginx combined | nginx-combined |
192.168.1.1 - - [10/Jul/2026:13:55:36 +0000] "GET /admin HTTP/1.1" 404 123 "-" "curl/7.68.0" |
| Apache common | apache-common |
192.168.1.1 - - [10/Jul/2026:13:55:36 +0000] "GET /admin HTTP/1.1" 404 123 |
| Apache combined | apache-combined |
192.168.1.1 - - [10/Jul/2026:13:55:36 +0000] "GET /admin HTTP/1.1" 404 123 "-" "curl/7.68.0" |
| Автоопределение | auto |
Пробует nginx-combined → apache-combined → apache-common |
Переменные окружения
| Переменная | По умолчанию | Описание |
|---|---|---|
IP_BLOCKER_LOG_PATH |
/var/log/nginx/access.log |
Путь к лог-файлу |
IP_BLOCKER_LOG_FORMAT |
auto |
Формат лога |
IP_BLOCKER_LOG_LEVEL |
debug |
Уровень логирования |
IP_BLOCKER_ANALYSIS_WINDOW |
5 |
Окно анализа (минуты) |
IP_BLOCKER_MAX_404 |
10 |
Порог 404 для блокировки |
IP_BLOCKER_MAX_REQUESTS |
100 |
Порог запросов для блокировки |
IP_BLOCKER_MAX_UNIQUE_URLS |
20 |
Порог уникальных URL |
IP_BLOCKER_BLOCK_DURATION |
60 |
Длительность блокировки (минуты) |
IP_BLOCKER_SERVER_TYPE |
nginx |
Тип веб-сервера (nginx/apache) |
IP_BLOCKER_DENY_PATH |
/etc/nginx/conf.d/blocked-ips.conf |
Путь для deny-конфига |
IP_BLOCKER_RELOAD_CMD |
nginx -s reload |
Команда перезагрузки сервера |
IP_BLOCKER_SYNC_ON_CHANGE |
true |
Перегенерировать deny-конфиг при изменении записей blocked_ips |
IP_BLOCKER_REPORT_EMAIL |
— | Email для отчётов |
IP_BLOCKER_MOONSHINE_ENABLED |
false |
Включить MoonShine-ресурс |
IP_BLOCKER_RETENTION_DAYS |
30 |
Дней хранить записи |
IP_BLOCKER_SCHEDULER_ENABLED |
false |
Авторегистрация ip:parse-log --block в планировщике |
IP_BLOCKER_SCHEDULER_SCHEDULE |
*/5 * * * * |
Cron-выражение для задачи парсинга |
IP_BLOCKER_CLEANUP_SUSPICIOUS_ENABLED |
false |
Авторегистрация ip:cleanup-suspicious в планировщике |
IP_BLOCKER_CLEANUP_SUSPICIOUS_SCHEDULE |
0 3 * * * |
Cron-выражение для задачи очистки |
Обнаружение сканеров (по User-Agent и путям)
По умолчанию пакет отслеживает запросы со статусом 4xx/5xx. Но сканеры (ExchangeScanner, zgrab, японские IoT-сканеры и т.п.) часто получают ответ 200 — например, когда SPA-приложение отдаёт index.html на любой путь. Такие запросы раньше не попадали в базу и IP не блокировался.
Начиная с v1.3.0 запрос считается подозрительным, если:
- статус >= 400, или
- User-Agent совпадает с шаблоном из
ip-blocker.suspicious.user_agents, или - путь URL совпадает с шаблоном из
ip-blocker.suspicious.paths
Шаблоны User-Agent — регистронезависимые подстроки с подстановкой *.
Шаблоны путей поддерживают wildcard-синтаксис Str::is() (например /owa*).
Списки настраиваются в опубликованном конфиге config/ip-blocker.php:
'suspicious' => [ 'block_on_user_agent' => true, // блокировать IP при совпадении UA (по умолчанию вкл.) 'block_on_path' => false, // блокировать IP при совпадении пути (по умолчанию выкл.) 'user_agents' => ['*exchangescanner*', '*zgrab*', '*sqlmap*'], 'paths' => ['/owa*', '/ews*', '/cgi-bin*', '/wp-admin*'], ],
Блокировка по паттерну (v1.6.0)
Начиная с v1.6.0 IP блокируется сразу при совпадении с подозрительным User-Agent, даже если сделан всего один запрос и ответ был 200 (раньше нужно было превысить пороги по количеству запросов). Это позволяет ловить сканирование ExchangeScanner/zgrab/etc. в один запрос.
block_on_user_agent = true— включено по умолчанию. Безопасно: UA вида*zgrab*,*exchangescanner*,*sqlmap*не встречаются у реальных пользователей.block_on_path = false— выключено по умолчанию. Широкие паттерны путей вроде/vendor*могут совпадать с легитимными ассетами админ-панели (например/vendor/moonshine/assets/app.js), поэтому включайте с осторожностью и после проверки спискаpaths.
Переменные окружения:
| Переменная | По умолчанию | Описание |
|---|---|---|
IP_BLOCKER_BLOCK_ON_UA |
true |
Блокировать IP при совпадении с подозрительным User-Agent |
IP_BLOCKER_BLOCK_ON_PATH |
false |
Блокировать IP при совпадении с подозрительным путём |
Исключение легитимных путей (v1.6.1+)
Пороговые лимиты (max_requests, max_unique_urls, max_404) считаются по
всем подозрительным запросам IP, включая статусы 4xx/5xx. Если владелец
сайта или реальные пользователи активно работают в админ-панели или личном
кабинете, их собственный IP может быть ошибочно заблокирован: например,
запросы к /admin/* и /vendor/moonshine/* совпадают с подозрительным
паттерном пути /vendor*, а при активной работе легко превысить порог
max_requests_per_window. Причём после блокировки deny-конфиг начинает
возвращать 403 на все запросы, парсер лога снова их фиксирует, и блокировка
самоподдерживается.
Чтобы этого избежать, добавьте пути приложения в exclude_paths:
'exclude_paths' => [ '/healthcheck', '/admin*', // админ-панель '/vendor/moonshine*', // ассеты админ-панели '/lk*', // личный кабинет '/storage*', ],
Исключённые пути не считаются подозрительными, даже если вернули 4xx/5xx
или совпали с подозрительным UA/путём. Проверка exclude_paths применяется
единообразно и в middleware, и в парсере лога (с v1.6.1), поэтому
ip:parse-log --block не будет пере-блокировать владельца.
Рекомендации для продакшена:
- Не занижайте пороги. Для реальных пользователей
IP_BLOCKER_MAX_REQUESTSниже ~100 иIP_BLOCKER_MAX_UNIQUE_URLSниже ~20 дают ложные блокировки активных сессий. Порогlimit: 5подходит только для тестов. - Всегда исключайте админку и кабинеты из отслеживания.
- После разблокировки владельца удалите его IP и из
blocked_ips, и изsuspicious_requests, а затем перегенерируйте deny-конфиг:
php artisan tinker --execute="\Zoolok\IpBlocker\Models\BlockedIp::where('ip','YOUR_IP')->delete();" php artisan tinker --execute="\Zoolok\IpBlocker\Models\SuspiciousRequest::where('ip','YOUR_IP')->delete();" php artisan ip:block --force
Устойчивость к мусорным строкам лога (v1.6.5)
В access-логи nginx попадают не только обычные HTTP-запросы, но и бинарные TLS-последовательности (например, от сканеров портов):
3.131.220.121 - - [03/Aug/2026:00:31:01 +0300] "\x16\x03\x01\x01\x23..." 400 0 "-" "-"
Парсер захватывал весь бинарный блоб как «метод запроса», что приводило к
ошибке записи в БД value too long for type character varying(10) (колонка
method в suspicious_requests имеет длину varchar(10)). Начиная с v1.6.5
метод запроса обрезается до 10 символов ещё в парсере, поэтому такие строки
сохраняются корректно и не роняют ip:parse-log --block.
Тестирование
composer test
Лицензия
MIT