«Это ТСПУ РКН, поделать ничего нельзя» — а проблема три месяца ждала в консоли сервера

Время расследования · 2 дня Причин найдено · 3 Команд для победы · 1

Клиент — онлайн-СМИ застройщика федерального уровня, несколько десятков публикаций в день, своя редакция. С июня сайт периодически становился недоступен. Подрядчик, который ведёт техподдержку, прислал такое сообщение:

Из переписки с подрядчиком, 26.08

Мы занимаемся техподдержкой и доработкой функционала сайта. Примерно с июня клиент жаловался на периодическую недоступность. Мы проверили все возможные причины — блокировок ни со стороны хостинга, ни со стороны VPS, ни со стороны сайта не найдено. Обращались в ТП хостера. После нескольких тестов — смены IP и прочего — мы сменили версию TLS-рукопожатия, сменили протокол HTTP, выполнили много других рекомендаций. Доступ со стороны обычных пользователей наладился.

Однако сейчас клиент жалуется на доступ нейросетей — то есть, то нет. Хостер утверждает, что дело в ТСПУ РКН: блокируются пулы айпи-адресов, у этих сервисов они каждый раз разные, отсюда и плавающая проблема. Технических ограничений нет.

Объяснение звучит убедительно: государственная система блокировок, случайные пулы адресов, подрядчик бессилен. Проверить его руками нельзя — а значит и опровергнуть тоже. Идеальный диагноз для того, чтобы закрыть тикет.

Через два дня выяснилось, что причин было три, все на этом же сервере, и ни одна не имела отношения к РКН.

01 · Улика. Таблица, которая не похожа на блокировку

Клиент прислал выгрузку из Google Search Console: столбец «Невыполненные запросы на сканирование» за 89 дней подряд, от 0,2% до 78%, среднее около 40%.

Если бы трафик резали по правилу — по User-Agent, по подсети, по решению регулятора — график выглядел бы как полка: 100%, пока правило действует, 0% вне его. Здесь распределение размазанное, без единого дня на нуле и без единого дня на ста. Отказ происходит вероятностно: конкуренция за ресурс, лимит соединений, таймаут, случайные баны части трафика.

89 дней без единого дня восстановления исключают версию с разовым инцидентом. Это состояние инфраструктуры.

02 · Подозреваемые. Семь гипотез до доступа к серверу

По одной таблице я выписал всё, что могло дать такой процент: PHP-FPM отдаёт robots.txt через самописный роутинг и упирается в лимит пула, лимиты nginx на соединения, временные баны fail2ban для подсетей Google, нестабильный IPv6, перегрузка БД волнами, антибот-периметр хостинга, проблемы TLS-хендшейка.

Список подозреваемых, не диагноз. Первая же команда на сервере расставила всё по местам:

root@server — сравнение протоколов

# IPv4
curl -4 -sS -o /dev/null -w 'code=%{http_code} connect=%{time_connect} total=%{time_total}\n' https://example.ru/robots.txt
code=200 connect=0.002458 total=0.066861
# IPv6
curl -6 -sS -o /dev/null -w 'code=%{http_code} connect=%{time_connect} total=%{time_total}\n' https://example.ru/robots.txt
curl: (7) Failed to connect: No route to host (3080 ms)

IPv4 отвечает за 66 миллисекунд. IPv6 падает мгновенно с «No route to host» — отказ маршрутизации на уровне ОС, до того как пакет вообще покинул сервер. Google и AI-краулеры при сканировании предпочитают IPv6, а если не получается то уже IPv4. Доля запросов, ушедшая по IPv6, проваливается стопроцентно. Отсюда размазанные 40% — доля, с которой краулер в конкретный день выбирал протокол, не работавший на этом сервере никогда.

MySQL показывал Max_used_connections = 2 из 151. dmesg по OOM и conntrack — пусто. robots.txt отдавался статикой nginx, PHP в цепочке не было. Шесть гипотез из семи отпали за пару минут проверок.

03 · Первая причина. Адрес существует в DNS и нигде больше

root@server

dig +short AAAA example.ru
2001:db8:1:2::68b5
ip -6 addr show eth0
только fe80::58ef:3fff:fecc:5162/64 scope link

DNS обещает адрес, которого на интерфейсе нет. В журнале сервера нашлась запись семинедельной давности:

/var/log/syslog

Jul 07 17:31:04 named[865]: listening on IPv6 interface eth0, 2001:db8:1:2::68b5#53

Локальный BIND при старте перебирает адреса интерфейса и слушает на каждом. Раз он забиндился именно на этот адрес седьмого июля, адрес на сервере тогда был. Кто-то поднял его вручную, через ip -6 addr add, без сохранения в постоянный конфиг. Такая команда живёт до первого рестарта сети. Дальше адрес исчез, а вернуть его было некому: SLAAC на интерфейсе отключён (accept_ra=0), без ручного вмешательства он не появляется сам.

04 · Ловушка. Netplan хранит два файла для одного интерфейса

Первая попытка закрепить адрес постоянно не сработала. Я добавил его в отдельный netplan-файл, netplan try подтвердил конфигурацию — адрес всё равно не появился.

Ответ нашёлся в networkctl status eth0:

networkctl status eth0

Link File:    /run/systemd/network/10-netplan-eth0.link
Network File: /run/systemd/network/10-netplan-ens3.network

На сервере жили два netplan-определения одного физического интерфейса под разными именами: старое, от установщика Ubuntu, с ключом ens3 (без адреса) и более новое, от cloud-init, с ключом eth0 (с нужным адресом). Netplan честно сгенерировал оба .network-файла. Systemd-networkd применяет для одного линка только первый совпавший файл по алфавиту, а ens3 идёт раньше eth0. Все правки в «eth0»-определениях молча игнорировались с самого начала, включая, вероятно, и оригинальную настройку от cloud-init.

Адрес пришлось прописать не в новый файл, а прямо в победивший ens3. После этого он встал и пережил перезагрузку.

05 · Вторая причина. Nginx слушал IPv6 не на том блоке

В конфиге nginx нашлась ровно одна строка listen [::]:80 — у служебного дефолт-блока server_name localhost, не у боевого vhost'а сайта. Добавил listen [::]:443 ssl http2; в нужный server-блок, перечитал конфиг. Внешняя проверка с другого сервера прошла до конца, TLS-хендшейк завершился успешно.

06 · Контрольная проверка. BGP снимает подозрение с провайдера — и с РКН

Прежде чем радоваться, стоило исключить два варианта: что успех был случайным (второй сервер в той же сети провайдера, трафик не выходил в публичный интернет), и что подрядчик всё-таки прав насчёт блокировки на уровне сети.

Публичный BGP-lookup показал: префикс, в который входит адрес сайта, анонсирован легитимно, с подписанным ROA. Маршрутизация в порядке. Ни хостер, ни какая-либо инспекция трафика на пути не резали этот префикс — иначе анонс был бы невалиден или недостижим, а он достижим отовсюду.

07 · Тупик, который не тупик. Google по-прежнему не может достучаться

Проверка URL в Search Console выдавала «URL недоступен Google», запрос на индексирование отклонялся с формулировкой «обнаружены ошибки». С внешнего сервера при этом соединение проходило безупречно.

Разница была в источнике запроса. Я запустил tcpdump на сетевой карте сервера и одновременно нажал «Test Live URL» в Search Console.

root@server — 40 секунд захвата

sudo timeout 40 tcpdump -i eth0 -n ip6
... 628 packets captured — всё исходящий трафик сервера,
ни одного входящего SYN на :443 ни с одного адреса Google

Пакет не долетал до сетевой карты вообще, при том что с обычного внешнего сервера SYN приходил и обрабатывался без проблем, а в access-логе тем временем спокойно писались обычные посетители и сканеры.

Это первая трещина в версии про ТСПУ РКН: если бы блокировался пул адресов на уровне сети, страдал бы весь входящий трафик из этого пула без разбора, а не только соединения к порту 443 по IPv6 от конкретных сервисов.

08 · Третья причина. Файрвол, о котором подрядчик не спросил

В том же дампе — побочный сюрприз: сервер сам открывал десятки TLS-соединений в секунду к случайным чужим адресам, обменивался парой пакетов и рвал соединение. Выглядело очень подозрительно. lsof и ps aux расставили точки: Imunify360, модуль безопасности хостинг-панели, легитимно синхронизирующийся с облаком репутации. Не инцидент, но повод проверить, раз уж открыли tcpdump.

Дальше — на уровень ниже nginx. Legacy iptables и ip6tables оказались пустыми, ACCEPT по умолчанию. Реальный фильтр жил в nftables:

nft list table ip6 filter

chain imunify360_country_blacklist {    match-set i360.ipv6.country-us src    counter packets 18 bytes 1516 jump imunify360_log_bl_country
}
chain imunify360_log_bl_country {    log prefix "im360-blacklisted-country" group 36005    counter packets 18 bytes 1516 drop
}

Imunify360 блокировал весь входящий IPv6-трафик из США по геолокации — 18 пакетов уже попали под дроп именно за время моих тестов. Инфраструктура Google Search Console, PageSpeed Insights, Rich Results Test и большинства AI-краулеров, которых клиент называл «нейросетями», физически размещена в США. Список доверенных краулеров в Imunify360 существует, с Google в нём же, но проверяется позже: гео-блэклист режет пакет раньше, чем модуль успевает распознать в нём легитимного бота.

Отсюда и «плавающие» ошибки, которые подрядчик приписывал РКН: часть запросов шла с адресов вне США или по IPv4 — проходила; часть шла по IPv6 из США — резалась на входе. Никакой связи с пулами и метками ТСПУ, обычная гео-блокировка одной страны, включённая в модуле безопасности, судя по всему, ещё во время той самой правки TLS и HTTP.

root@server — снято из консоли, без панели

sudo imunify360-agent blacklist country delete US
sudo nft list table ip6 filter | grep -A2 country_blacklist
chain imunify360_country_blacklist { }

Повторный curl -6 с американского сервера дошёл до TLS handshake, Server hello за секунды. В течение часа доступ подтвердили все три инструмента Google: PageSpeed Insights, Search Console и Rich Results Test.

GSC через час
GSC через час

09 · Итог. Три причины, каждая закрывала бы дело сама

1. IPv6-адрес существовал только в DNS: поднимался вручную один раз, автовосстановление было отключено.

2. Даже с адресом на месте nginx не принимал HTTPS по IPv6 на боевом vhost'е — слушал только служебный дефолт-блок.

3. Даже с рабочими первыми двумя пунктами гео-блокировка США резала весь входящий IPv6-трафик из страны, где физически стоит инфраструктура Google и большинства AI-сервисов.

Убери одну из трёх — картина осталась бы прежней: хронические 40% отказов без внятной закономерности, и версия про ТСПУ РКН продолжала бы звучать правдоподобно.

Методологический вывод

У версии подрядчика был изъян, для которого не требовался доступ к серверу: настоящую блокировку на уровне сети нельзя починить одной командой на своём сервере. Если бы дело было в ТСПУ, imunify360-agent blacklist country delete US не изменил бы ничего — трафик резался бы выше, вне зоны досягаемости root-доступа на конкретной VPS.

У нас проблема исчезла мгновенно после одной локальной команды. Это и есть тест: если объяснение звучит как «ничего нельзя сделать», первый вопрос — какая конкретно команда на этом сервере ничего не изменит, если объяснение верно. Нет такой проверенной команды — объяснение не проверено, а просто удобно.

Чек-лист. Как проверять такое у себя

порядок диагностики, сверху вниз:

# 1. Сравнить протоколы
curl -4 -v https://example.com/robots.txt
curl -6 -v https://example.com/robots.txt
# 2. Адрес физически на интерфейсе?
ip -6 addr show
# 3. Какой netplan-конфиг реально победил
networkctl status eth0
cat /run/systemd/network/10-netplan-*.network
# 4. nginx слушает IPv6 именно на нужном блоке?
sudo ss -6 -ltnp | grep :443
sudo nginx -T | grep 'listen \[::\]'
# 5. Маршрут легитимен на стороне провайдера?
— публичный BGP-lookup по адресу
# 6. Пакет вообще доходит до карты во время живого теста?
sudo tcpdump -i eth0 -n ip6
# 7. Если доходит и падает — ищем файрвол, не только в iptables:
sudo nft list tables
sudo nft list table ip6 filter

Пункт 7 — самый частый провал диагностики. Iptables показал пустые цепочки, и файрвол чуть не списали со счетов. Реальные правила жили в параллельной системе с другим названием таблиц.

Search Console какое-то время кэширует старый статус robots.txt на уровне всего домена и не пересчитывает его при каждой ручной проверке — в этом случае обошлось часом, но закладывать стоит больше.