Self-built middleware, которое считает “подозрительность” каждого IP по набору сигналов и принимает решение: пропустить, замедлить (tarpit) или заблокировать.
Сигналы
Tier 0 — до HTTP (транспортный уровень)
| Сигнал | Что значит | Вес |
|---|---|---|
| JA4 fingerprint не в allowlist браузеров | Не браузер — скрипт, бот, HTTP-клиент | +40 |
| JA4 в blocklist (Python requests, Go http) | Известная scraping-библиотека | +60 |
| Нет HMAC-подписи | Запрос не от легитимного фронтенда | +30 |
| Невалидная HMAC-подпись | Попытка подделки или устаревший ключ | +50 |
Tier 0 сигналы — самые надёжные, т.к. TLS fingerprint нельзя подделать без замены TLS-стека.
Tier 1 — проверяются на каждый запрос (заголовки)
| Сигнал | Что значит | Вес | FP-риск |
|---|---|---|---|
Нет Sec-Fetch-Site | Не браузер (90%+ браузеров шлют) | +10 | Низкий — старый Safari <16.4 |
Sec-Fetch-Site != same-origin | Запрос с чужого сайта | +15 | Низкий — SPA всегда same-origin |
| Нет User-Agent или пустой | Браузер всегда шлёт, даже curl | +15 | Очень низкий |
| Известный бот UA (python-requests, Go-http-client, scrapy) | Скрейпер с дефолтным UA | +20 | Очень низкий — не browser UA |
| Нет Accept-Language | Боты часто не шлют | +5 | Низкий |
| Нет cookies (кроме auth/home путей) | Бот без сессии | +10 | Средний — первый визит |
Sec-Fetch-* заголовки
Добавляются браузерным движком автоматически (forbidden header name — нельзя установить через JS
fetch()). Chrome 80+, Firefox 90+, Safari 16.4+. Серверные HTTP-клиенты могут подделать вручную, но 99% ботов этого не делают (SearXNG PR #3965).
Tier 2 — требуют хранения состояния per-IP (Redis)
| Сигнал | Что значит | Вес | Окно |
|---|---|---|---|
| > 60 req/min | Слишком быстро для человека | +20 | 1 мин |
| > 300 req/5min | Sustained scraping | +30 | 5 мин |
| > 20 уникальных путей/мин | Перебор API | +15 | 1 мин |
| IP уже был в honeypot | Известный скрапер | +25 | 24ч |
Реализация через Redis pipeline() — один round-trip (~2ms): INCR для rate, SADD+SCARD для path diversity. SET лучше HyperLogLog при малой кардинальности (<100 путей) — точнее и экономнее по памяти.
Honeypot IP кэш — in-memory Set<string>, обновляется cron каждые 5 минут из БД. Не Redis — экономит round-trip.
Система скоринга
Два подхода к скорингу:
Per-request scoring (рекомендуется)
Каждый запрос оценивается изолированно — сумма сигналов Tier 1 + Tier 2. Без накопления между запросами — проще, предсказуемее.
Пороги (graduated response):
< 0 → whitelisted (Googlebot, APK, вебхуки)
0-29 → NORMAL — обычный ответ
30-49 → MONITOR — только логи + Prometheus метрики
50-69 → THROTTLE — задержка 0.5-2 сек (линейно по score)
70+ → TARPIT — 1 байт/сек, до 2 мин
Graduated response — индустриальный стандарт. Cloudflare, Akamai, DataDome используют аналогичный подход.
Accumulated scoring (альтернатива)
Score накапливается для IP с decay (*= 0.95/мин). Подходит для long-session анализа, но сложнее в реализации и отладке. Для большинства проектов per-request достаточно.
Tarpit
Вместо 403 — держать соединение, отвечать медленно. Почему эффективнее блока:
- Бот не знает что обнаружен — получает 200, просто медленно. Нет сигнала менять IP.
- Занимает ресурсы бота — его потоки висят в ожидании. 10 параллельных запросов с задержкой 10 сек = 1 req/sec вместо 100.
- Автоматический бэкофф — многие библиотеки сами снижают rate при медленных ответах.
Tarpit как DoS
Tarpit держит Node.js-соединение. При массовой атаке — ограничить
MAX_TARPITS(например 100), при превышении — жёсткий 429.
Хранение состояния
| Решение | Когда | Паттерн |
|---|---|---|
| Map в памяти | 1 инстанс, < 100k IP | Map<string, {count, lastSeen}> с TTL-sweep |
| Redis | Cluster mode, несколько воркеров | INCR + EXPIRE для rate; SADD+SCARD для path diversity. Один pipeline() = 1 round-trip |
Redis sliding window: INCR sus:rate:{ip}:{minute} + EXPIRE 120s. TTL = 2× окна для перекрытия. Память: 1000 IP × 3 ключа × ~100 байт = ~300 KB.
Защита от ложных срабатываний
- Авторизованные пользователи — score
*= 0.5, но не обнулять (см. авторизованные скрейперы) - Whitelist ботов — Googlebot, Yandexbot (проверять reverse DNS)
- Блок невозможен по одному сигналу — максимум одного = 30, порог блока = 80
- Первые 2 недели — режим “только логи”, не блокировать
- Decay критически важен — пользователь, быстро пролиставший 5 страниц, не блокируется навсегда
Кто похож на бота, но не бот
| Ситуация | Решение |
|---|---|
| RSS-ридеры | Whitelist известных UA |
| PWA / мобильные приложения | Cookie = легитимный, обнулить score |
| Корпоративный NAT (200 человек за 1 IP) | Не блокировать IP, только замедлять; учитывать cookie |
| Пользователь с adblocker | Нет статики ≠ бот, только +10-20 |
Связанные заметки
- Защита API от скрейпинга — общая стратегия
- HMAC-подпись API запросов — предыдущий уровень защиты
- CrowdSec — внешний инструмент для IP-репутации