
Две недели назад я закончил домашний проект — небольшую утилиту под Windows, — и решил, что ей нужна страница в интернете. Взял самый дешёвый VPS, так как много мощностей для моих задач не требуется, поднял nginx, выложил статическую страницу без единого внешнего скрипта. Счётчики вроде популярных метрик ставить не хотел принципиально: утилита не собирает никаких данных о пользователе, и было бы странно начинать “слежку” прямо со страницы о ней. Поэтому аналитику я сделал самую честную из возможных — разбор собственных логов nginx.
Через трое суток открыл сводку и увидел: 273 уникальных адреса, из них 165 открыли главную страницу. Для проекта, о котором не знает ровно никто — ни одной ссылки нигде, ни единого анонса — это выглядело как маленькое чудо. Чуда, разумеется, не было: живых посетителей за эти двое суток набралось около двадцати, а из четырёх скачиваний моего установщика ни одно не сделал человек!
Дальше больше: как я это выяснил, почему первый подход не сработал и что в итоге стоит на сервере?
Что вообще происходит на свежем домене?
Первым делом я посмотрел не на посещаемость, а на распределение кодов ответа — оно оказалось красноречивее любой сводки. Из 2 129 запросов успешными были только 613, а больше двух третей пришлось на ошибки. Причём ошибки не случайные, и это сразу видно по тому, за какими адресами ходили клиенты.
Код ответа
Сколько раз
403
934
200
613
404
487
304
400
405
401
Вот десятка самых популярных путей среди этих ошибок — классический перебор в чистом виде:
/.env
/api/.env
/.env.production
/.env.backup
/.git/config
/web/.env
/tmp/.env
/server/.env
/public/.env
/private/.env
Дальше по списку идут phpinfo.php во всех мыслимых и не мыслимых подпапках: /wp-admin/, /vendor/, /administrator/. Ботам нужен файл с паролями к базе, выгрузка git-репозитория или забытая админка — они обходят диапазоны адресов и пробуют наугад найти дыру в защите. На моём сайте нет ни PHP, ни базы, ни WordPress, только статические файлы, поэтому все попытки ушли в пустоту. Но проблема в том, что запросы-то сделаны, и в наивную статистику посещаемости они попадают наравне с живыми людьми.
Отдельно порадовали четыре адреса, давшие по: 276, 276, 269 и 269 запросов. Каждый из них за пару часов простучал под три сотни путей, и в сводке это выглядит как четыре заинтересованных очень настойчивых “посетителя”. Числа почти совпадают неспроста: это один и тот же сканер, запущенный с четырёх машин одного облачного провайдера. По отдельности каждый выглядит как посетитель с уникальным адресом, а вместе они дали больше половины всего «трафика» за сутки.
Попытка первая: фильтровать по User-Agent
Решение, которое приходит в голову первым: а что если отсеять всех, кто честно назвался роботом. В nginx - это делается картой, которая помечает запрос, плюс второй картой, дающей обратный флаг. Вторая нужна из-за особенности директивы if=: nginx пишет строку в лог, если переменная непустая и не равна нулю, поэтому «человечность» удобнее считать явно, а не полагаться на интуицию.
Дальше в блоке server пишем два лога вместо одного: полный — для разбора инцидентов и «человеческий» — для статистики.
Запустил, подождал, посмотрел — и разочаровался. По User-Agent отсеялось 267 запросов из 2 129, то есть около 12 % . При этом я своими глазами видел в логе адреса, которые ломились в /.env и представлялись как браузер Chrome под Windows. Вывод получился обидный, но полезный: честные роботы представляются честно, а нечестные — нет. Казалось бы очевидно? Но не все так прозрачно как кажется на первый взгляд. Поисковым системам, ИИ-краулерам и утилитам вроде curl репутация нужна, их можно заблокировать по имени, и они это знают. Сканеру уязвимостей на ресурсе репутация не нужна вовсе — он просто подставляет строку настоящего браузера, и любой фильтр по имени агента обходится без проблем.
Попытка вторая: смотреть не кто представился, а что сделал
Не сдаемся и копаем дальше! Я зашёл с другой стороны. Меня же интересует: не «как назвался клиент», а «был ли это человек с браузером»? А браузер отличается от скрипта поведением: получив HTML, он немедленно идёт за таблицей стилей, шрифтами и картинками, иначе ему нечего показать на экране. Сканеру содержимое страницы неинтересно, ему нужен только сам факт ответа, поэтому за оформлением он не возвращается. Отсюда косвенный признак: клиент считается живым, если забрал и страницу, и её оформление.
Проверяется это по логу одним проходом awk — сначала собираем адреса, которые хоть раз запросили стили или картинки, потом оставляем только их строки:
Результат отрезвил окончательно. Двадцать один живой клиент против двухсот семидесяти трёх «уникальных посетителей» — это ровно 8 % от того, что показывал наивный подсчёт, и в двенадцать раз меньше, чем я увидел в первой сводке. Причём фильтр по имени агента, на который я неспешно потратил вечер, почти ничего не изменил: 168 против 165 — разница в пределах шума. Всю работу сделал один поведенческий признак, который пишется в пять строк.
Как считать
Сколько «посетителей»
Все уникальные адреса
273
Открыли главную и получили 200
165
Отсеяны только представившиеся роботы
168
Забрали страницу вместе со стилями
И это ещё не конец истории. Я прогнал оставшуюся двадцатку через определение типа сети — хотелось понять, откуда адрес: из домашнего интернета или из дата-центра. Выяснилось, что среди этой двадцатки четверо тоже роботы: два обращения от краулера одной ИИ-компании, по одному — от поискового робота и краулера другой ИИ-компании. Они грузят стили и картинки совершенно честно, потому что рендерят страницу целиком, и поведенческий признак их не ловит. Больше половины оставшихся адресов принадлежали хостинг-провайдерам, то есть тоже не людям с домашним интернетом, а чему-то запущенному на арендованной машине. Реальных живых людей за двое суток набралось около полудесятка, включая меня самого.
Самое неприятное открытие: скачивания
Дальше я посмотрел на то, ради чего сайт вообще существует, — на кнопку «Скачать». Установщик весит около 67 МБ и отдаётся с сервера напрямую, так что каждое скачивание видно в логе с точным числом переданных байт. За те же двое суток к файлу было 11 обращений, из которых 4 закончились полной отдачей. Четыре скачивания за два дня для проекта без единого анонса — я бы порадовался, если бы не посмотрел, кто их сделал.
Кто скачал файл целиком
Сколько раз
Краулер одной ИИ-компании
Я сам, проверяя, что кнопка работает
Пользовательских сторонних скачиваний — ноль. Остальные семь обращений оборвались на первых сотнях килобайт: кто-то из облака дёрнул начало файла и отвалился, поисковый робот забрал 278 КБ, видимо, определяя тип содержимого. Отдельно стоит посчитать расход: три полных выкачивания — это 200 МБ трафика, потраченных на то, чтобы бинарник уехал в чей-то датасет. На дешёвом VPS с лимитом это уже заметно, а при раздаче с платного объектного хранилища превратилось бы в счёт.
Отсюда вывод , который я бы вынес отдельно для всех, кто раздаёт файлы: счётчик скачиваний с релизной страницы или из хранилища — не метрика. Он считает всех, кто подключился, и не отличает человека от робота. Если хочется знать реальное число, считать придётся самому: завершённую отдачу файла по объёму переданных байт и только с адреса, который до этого вёл себя как браузер. При таком подходе конечно не будут учитываться живые скачивания, не доведенные до конца - передумал, но думаю, что это не самый худший компромисс.
Что я сделал с этим дальше
Понимание пониманием, но 70 % мусорных запросов никуда не делись: они жгут процессор, забивают логи и мешают смотреть на реальную картину. Городить капчу или ставить перед сайтом чужой антибот-сервис я не хотел — это посредник между читателем и статикой, да и странно бороться со слежкой, добавляя ещё одного наблюдателя. Поэтому обошёлся штатными средствами: четыре меры, от самой мягкой к самой жёсткой, и ни одна из них не мешает поисковой индексации.
Первое — обрывать заведомо чужие запросы
У меня нет и не будет ни PHP, ни WordPress, ни git на сервере, поэтому любой такой запрос заведомо чужой. Отдавать на него честную страницу 404 незачем: это лишняя работа, лишний ответ и лишняя строка в общем логе. Нестандартный код 444 специфичен для nginx и означает «закрыть соединение, ничего не отвечая» — сканер не получает никакой обратной связи, а я получаю отдельный лог с адресами тех, кто туда лез. Приятный бонус для аналитики.
Второе — ограничение частоты
Человек, открывая страницу, делает десяток запросов разом (HTML, стили, иконки, картинки) и затихает, а перебор идёт ровным потоком в сотни запросов. Эта разница ловится штатным модулем: всплеск в 30 запросов покрывает загрузку страницы с запасом, а равномерный поток упирается в лимит и получает 429. Я проверил на себе — пятнадцать запросов подряд к разным файлам прошли без единого отказа.
Третье — банить упорных
Раз мусорные запросы теперь пишутся в отдельный лог, для fail2ban не нужен хитрый шаблон: подозрительна любая строка в этом файле, надо лишь вытащить из неё адрес. Три попытки за час — и клиент уходит в бан на сутки. Ложные срабатывания тут практически исключены: живой человек в /.env не заходит никогда, а поисковые роботы такие пути не запрашивают, так что индексации это не вредит.
Четвёртое — попросить роботов не трогать тяжёлый файл
Те, кто уважает robots.txt, а поисковые и ИИ-краулеры его уважают, послушаются. Заодно я закрыл целиком несколько сервисов SEO-разведки: они выкачивают сайт ради чужой аналитики и не приносят мне ничего, кроме трафика.
Что в итоге
Через сутки после всех мер логи стали заметно чище, но главное даже не в этом: я перестал обманывать себя цифрами. Сейчас у меня три уровня статистики вместо одной сводки: живые посетители, расширенный отчёт без опознанных роботов и полный трафик со всеми сканерами. Решения принимаются по первому, второй нужен для сравнения, третий — чтобы разбирать инциденты.
Пять вещей, которые я вынес из этой истории
1. Уникальные адреса — это не посетители. На свежем сайте без единой входящей ссылки разница оказалась двенадцатикратной, и весь «трафик» состоял из роботов.
2. Фильтр по User-Agent отсекает только вежливых. Он поймал 12 % запросов и пропустил всех, кто маскируется под браузер, то есть именно тех, из-за кого цифры и раздуваются.
3. Поведение надёжнее самоназвания. Один признак: «забрал страницу вместе со стилями» дал больше, чем список из полусотни известных ботов, и его не нужно дописывать при появлении новых сканеров.
4. Счётчик скачиваний врёт по умолчанию. Пока вы не считаете завершённую отдачу файла с адреса живого посетителя, вы считаете роботов и оборванные соединения.
5. Смотреть надо в свои логи. Все ответы уже лежали у меня на диске — я двое суток доверял агрегированной сводке вместо того, чтобы один раз взглянуть на распределение кодов ответа и другую информацию. Да долго и нудно, но результат повысит общую эффективность и достоверность данных.
Про ИИ-краулеров добавлю отдельно, чтобы это не прозвучало обвинением: они ходили по robots.txt, представлялись честно и ничего не ломали, претензий к ним у меня нет. Просто если вы раздаёте с сайта тяжёлые файлы, будьте готовы, что скачают их не только люди, и решите заранее, устраивает вас это или нет. Меня в итоге устроило чтение страниц, а бинарник я от них закрыл.
Спасибо, если прочитали до конца!
Надеюсь, статья окажется для вас полезной.