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Слишком быстро для человека+201 мин
> 300 req/5minSustained scraping+305 мин
> 20 уникальных путей/минПеребор API+151 мин
IP уже был в honeypotИзвестный скрапер+2524ч

Реализация через 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 — держать соединение, отвечать медленно. Почему эффективнее блока:

  1. Бот не знает что обнаружен — получает 200, просто медленно. Нет сигнала менять IP.
  2. Занимает ресурсы бота — его потоки висят в ожидании. 10 параллельных запросов с задержкой 10 сек = 1 req/sec вместо 100.
  3. Автоматический бэкофф — многие библиотеки сами снижают rate при медленных ответах.

Tarpit как DoS

Tarpit держит Node.js-соединение. При массовой атаке — ограничить MAX_TARPITS (например 100), при превышении — жёсткий 429.

Хранение состояния

РешениеКогдаПаттерн
Map в памяти1 инстанс, < 100k IPMap<string, {count, lastSeen}> с TTL-sweep
RedisCluster 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.

Защита от ложных срабатываний

  1. Авторизованные пользователи — score *= 0.5, но не обнулять (см. авторизованные скрейперы)
  2. Whitelist ботов — Googlebot, Yandexbot (проверять reverse DNS)
  3. Блок невозможен по одному сигналу — максимум одного = 30, порог блока = 80
  4. Первые 2 недели — режим “только логи”, не блокировать
  5. Decay критически важен — пользователь, быстро пролиставший 5 страниц, не блокируется навсегда

Кто похож на бота, но не бот

СитуацияРешение
RSS-ридерыWhitelist известных UA
PWA / мобильные приложенияCookie = легитимный, обнулить score
Корпоративный NAT (200 человек за 1 IP)Не блокировать IP, только замедлять; учитывать cookie
Пользователь с adblockerНет статики ≠ бот, только +10-20

Связанные заметки