Вчера переехал на свой сервер. Сегодня утром обнаружил, что сайт у меня не открывается. Ни из дома, ни с телефона.
Сервер при этом в Москве, у российского хостера. И с ним всё в порядке.
Дальше про то, как я полдня искал причину, какие шесть версий проверил и выбросил, и чем всё кончилось. Если симптомы похожие, в конце готовая методика: что запускать и как читать результат.
Симптомы
Сайт статический, Next.js, отдаётся через nginx. Рядом два Docker‑контейнера: чат‑виджет и API для демок. Сервер 2 CPU, 4 ГБ, Москва.
Что я видел:
через VPN сайт открывался, но через раз;
без VPN не открывался вообще;
с телефона по мобильному тоже нет;
ping до сервера при этом шёл идеально.
Последний пункт и сбивал с толку. Сеть до сервера есть. Пакеты ходят. А сайт не грузится.
Первое, что я сделал не так
Скажу сразу: час я потратил впустую. Гадал вместо того, чтобы мерить.
Первая версия была про VPN. Внешний адрес показывал Франкфурт, трафик до Москвы шёл через Германию, задержка 100 мс. Вроде логично: туннель теряет пакеты, отсюда и «через раз».
$ curl -s https://ipinfo.io/json { "ip": "203.0.113.10", "city": "Frankfurt am Main", "country": "DE", "org": "AS47447 23M GmbH" }
Версия красивая. И неверная. Без VPN сайт не открывался вообще, а этого она не объясняла.
Вывод, до которого я дошёл поздно: если у гипотезы остаётся симптом, который она не объясняет, гипотеза неверна. Не «почти», не «частично». Неверна.
Как надо было: мерить серией
Одиночная проверка бесполезна, когда отказ плавающий. Нужна серия. Вот с чего надо было начинать:
ok=0; fail=0 for i in $(seq 1 25); do c=$(curl -s -o /dev/null -m 10 -w "%{http_code}" https://titov-ai.ru/) [ "$c" = "200" ] && ok=$((ok+1)) || fail=$((fail+1)) done echo "ok=$ok fail=$fail"
Через VPN получилось ok=18 fail=7. Двадцать восемь процентов запросов не проходит.
Дальше нужна контрольная группа. Без неё замер не значит ничего. Проверяем другие хосты через тот же канал:
google.com → 15 из 15 timeweb.com → 12 из 12 (сайт моего же хостера) мой сервер → 18 из 25
Вот теперь есть факт. Канал живой, чужие сайты открываются, включая сайт того самого хостера. Не открывается конкретный адрес.
Странность, которая всё запутала
Разложил проверку по уровням и получил картину, которая противоречит сама себе:
ping (30 пакетов) → 0% потерь чистый TCP-коннект :443 → 20 из 20 чистый TCP-коннект :22 → 20 из 20 SSH-сессия → работает curl HTTPS → 6 провалов из 20 curl HTTP :80 → 5 провалов из 15
TCP устанавливается всегда. SSH работает. А обычный HTTP‑запрос падает в трети случаев.
Отказ выглядел так:
curl: (28) Connection timed out after 10000 milliseconds time_connect = 0.000000
Ноль в time_connect значит, что соединение не установилось вообще. SYN ушёл, ответа нет. Не разрыв на середине передачи. Молчание.
Шесть версий, которые не подтвердились
Все проверил и выбросил. Пишу целиком, потому что отрицательный результат тоже результат.
Авария на ноде хостера. В панели реально висело уведомление об аппаратном сбое. Отпала, когда я зашёл на сервер: load average 0.00, память свободна, nginx с нулём перезапусков, контейнеры Up 10 часов. Сбоило бы железо, страдал бы и SSH. Не страдал.
Nginx или Docker не принимают соединения. Разумно, если бы падал только веб. Но чистый TCP‑коннект на 443 давал 20 из 20. При переполнении accept‑очереди он бы тоже падал.
MTU и фрагментация. Path MTU до сервера оказался 1300 вместо 1500, типичная подпись туннеля. Отпала на контрольном замере: до старого хостинга MTU был точно такой же, и там всё работало.
Сертификат. Отпала сразу. Нулевой time_connect значит, что до TLS дело не доходит, а сертификат читается уже после установки соединения.
Отсутствие AAAA‑записи. У моей машины IPv6 вообще нет, curl -6 не находит адреса. Все замеры шли по IPv4, форсированный curl -4 давал те же потери.
IP в чёрных списках. Шесть DNSBL, включая Spamhaus и SpamCop. Все чистые.
Что дало ответ
Вот это надо было делать первым. Заходим на сервер и снимаем дамп трафика, пока снаружи бьём запросами:
# на сервере systemd-run --unit=cap --collect \ tcpdump -i any -nn -c 3000 -w /tmp/cap.pcap tcp port 443
systemd-run тут не для красоты. SSH‑сессия рвалась каждые пару минут, а так процесс переживает обрыв.
Снаружи запускаю двадцать пять запросов, три падают. Смотрим, что видел сервер в момент падения:
09:01:32.377365 In IP 203.0.113.10 > сервер.443: Flags [S] 09:01:32.377468 Out IP сервер.443 > 203.0.113.10: Flags [S.] 09:01:32.476632 In IP 203.0.113.10 > сервер.443: Flags [.] ack
SYN пришёл. Сервер ответил SYN‑ACK через одну десятитысячную секунды. Рукопожатие завершилось. А curl на моей стороне в этот момент показывал таймаут.
Всего пришло 25 SYN на 25 запросов. Ни один не потерялся по пути туда. Сервер ответил на все. Терялся ответ, обратно.
Развязка
Оставалось проверить очевидное, до чего я дошёл последним: как ведёт себя сервер с реального канала, мимо VPN. В Windows это делается флагом --interface:
# 192.168.1.50 это реальный интерфейс, не туннель curl --interface 192.168.1.50 https://titov-ai.ru/
Результат:
ping до сервера → 6 из 6, 16 мс, 0% потерь TCP на 443 → 0 из 10 TCP на 443 по IP → 0 из 8 google.com → 3 из 3 timeweb.com → 3 из 3
ICMP проходит идеально. TCP на 80 и 443 не проходит ни разу. Другие сайты с того же интерфейса работают.
Это фильтрация. Не потери, не перегрузка. Избирательная блокировка TCP к конкретному адресу при живом ICMP.
Хостер подтвердил, когда я прислал замеры. Их проверка снаружи: порт открывается, nc -zv отвечает succeeded, но соединение рвётся на TLS‑хэндшейке. Curl виснет сразу после Client hello, браузер даёт ERR_CONNECTION_RESET. Изнутри их сети сайт при этом отдаёт HTTP/2 200.
Ответ поддержки: «изменения в настройках фильтрации со стороны магистральных провайдеров, повлиять на это мы не можем».
Почему именно мой адрес
Вот тут самое интересное. Ради этого стоило копать.
Смотрим, кому принадлежит блок:
$ curl -s "https://stat.ripe.net/data/prefix-overview/data.json?resource=201.x.x.x" \ | jq '.data.block.desc, .data.asns' "LACNIC (Status: ALLOCATED)" [{"asn": 9123, "holder": "TimeWeb-AS JSC \"TIMEWEB\""}]
LACNIC это регистратор Латинской Америки. Адрес российского хостера, физически стоящий в Москве, формально принадлежит латиноамериканскому диапазону 201.0.0.0/8.
Хостеры выкупают такие блоки легально. Свободных адресов в RIPE давно нет, а IPv4 нужны. Юридически всё чисто: в базе RIPE запись TW-Cloud, страна RU, организация в Петербурге.
Но магистральные провайдеры фильтруют по спискам. И нетипичные для России диапазоны попадают под них целиком. Хватит того, что сосед по подсети когда‑то дал повод.
Попросил у хостера другой адрес. Выдали из семейства 200.x. Проверяю:
$ curl -s https://ipinfo.io/200.x.x.x/json { "city": "Ji Paraná", "region": "Rondônia", "country": "BR" }
Бразилия. Снова LACNIC. Те же грабли.
Чем кончилось
Переехал к другому хостеру. Перед оплатой проверил их диапазоны через 2ip.io/ru/as/: RIPE, зарегистрированы на Россию. Потом проверил доступность их сетей со своего канала. Пять из пяти.
После переезда тот же замер:
до: 0 из 10 задержка 100 мс после: 12 из 12 задержка 25 мс
Перенос занял часа три. Всё было в Docker, так что механика простая: два архива с проектами, .env с ключами, сертификат (он привязан к домену, перевыпускать не надо), статика сайта, DNS.
Методика, если симптомы похожие
По шагам. Первые три занимают пять минут и отсекают половину версий.
Мерьте серией, а не одиночным запросом. Плавающий отказ на одиночной проверке выглядит как случайность.
Берите контрольную группу. Другие хосты через тот же канал. Без этого замер ничего не доказывает.
Разложите по уровням: ICMP, чистый TCP, TLS, HTTP. Место, где ломается, сужает круг втрое. Ping не идёт, значит маршрут или хост лежит. Ping идёт, TCP нет, это фильтрация. TCP идёт, TLS рвётся, это фильтрация по SNI или DPI. Всё идёт, падает только HTTP, вот тогда смотрите свой сервер.
Проверьте обходной путь. Есть VPN, сравните с ним и без него. В Windows реальный интерфейс задаётся через curl --interface, адреса смотреть в ipconfig.
Снимайте tcpdump на сервере. Он отвечает на главный вопрос: доходят ли SYN и отвечает ли сервер. Остальное догадки.
Проверьте, чей диапазон. Одна команда:
curl -s "https://stat.ripe.net/data/prefix-overview/data.json?resource=ВАШ\\_IP" | jq .data.block
Увидели LACNIC, AFRINIC или APNIC у российского хостера, причина скорее всего найдена. Просите адрес из RIPE.
Что вынес
Гипотеза, которая не объясняет все симптомы, неверна. Я трижды строил версию, объясняющую часть картины. Трижды она разваливалась о факт, который я решил считать неважным.
Дамп с сервера стоит часа гаданий. Полдня я перебирал версии снаружи, хотя SSH работал с самого начала. Одна команда tcpdump дала однозначный ответ там, где шесть гипотез дали шесть тупиков.
Хостер видит только свою сеть. Первая линия честно пропинговала сервер изнутри стойки, получила 0,68 мс и написала, что всё работает. Формально они правы. Сдвинулось, только когда я прислал таблицу: столько успешных, столько таймаутов, вот трассировка, вот дамп. С цифрами не поспоришь.
Спрашивайте про диапазон до оплаты. Одна строчка в чат поддержки, «из какого блока выдаётся IPv4», экономит день переезда. Я узнал это на своих ошибках. Вы теперь знаете заранее.