MAATRIX / Блог / Половина клиентов не могла загрузить файл больше мегабайта

Половина клиентов не могла загрузить файл больше мегабайта

MAATRIX

Пользователи жаловались по-разному: у одних всё грузилось нормально, у других любой файл больше мегабайта — фотография, экспорт CSV, вложение к тикету — просто зависал на середине и никогда не долетал. Код клиента и сервера был одинаковым для всех, реплицировать баг на своей машине не получалось неделю. Разбираем, как случайное на первый взгляд поведение оказалось учебным примером поломки Path MTU Discovery — и что с этим делать, если у вас похожая картина.

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

Первые репорты выглядели как шум. Часть пользователей писала «не работает загрузка», часть молчала — у них всё было в порядке. Разработчики попробовали воспроизвести баг на тестовом стенде — загрузили файл на 5 МБ с офисного ноутбука, всё прошло штатно. Баг записали в раздел «не воспроизводится, закрыть как invalid» — классическая ошибка, потому что проблема была не в клиенте и не в коде, а в конкретных сетевых путях, по которым ходил трафик до сервера.

Что было известно на старте:

  • Маленькие запросы (логин, получение списка файлов, текстовые сообщения в чате) работали у абсолютно всех без исключения.
  • Загрузка файлов из мессенджеров и коротких вложений (пара сотен килобайт) — тоже без нареканий.
  • Файлы от мегабайта и выше — зависали у части пользователей, причём именно зависали: не было ни ошибки 500, ни разрыва соединения, прогресс-бар просто замирал на одном месте и висел до таймаута клиента (обычно 30–60 секунд), после чего клиент показывал «не удалось загрузить файл».
  • Проблема была стабильно воспроизводима у пострадавших пользователей и стабильно НЕ воспроизводилась у остальных — то есть это не было редкой случайностью или флаппингом канала, у каждого конкретного человека поведение было устойчивым.

Версия «слабый канал у части пользователей» тоже быстро отпала: среди пострадавших были и офисные подключения с гигабитным интернетом, и мобильные — закономерности по «качеству» связи не было. Была другая закономерность, которую заметили не сразу.

Ложный след: искали закономерность не там

Первые два дня расследования ушли на сравнение пострадавших и непострадавших пользователей по всем очевидным признакам: браузер, ОС, версия клиентского приложения, регион, тип интернета. Ничего не сходилось стабильно — среди тех, у кого не работало, были и Windows, и macOS, и Linux, четыре разных региона. Заподозрили race condition в клиентском коде загрузки, посмотрели логи фронтенда на предмет необработанных промисов и таймаутов запроса — ничего воспроизводимого не нашли.

Переломный момент случился, когда один из пострадавших пользователей прислал скриншот: загрузка файла на 900 КБ прошла успешно, а следующая попытка с файлом на 1.1 МБ того же формата зависла на той же машине, в той же сети, с разницей в тридцать секунд между попытками. Это исключило версию про сеть провайдера в целом (иначе не работало бы вообще ничего) и версию про клиентский баг (код запроса был одинаковым для обоих файлов, менялся только размер тела).

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Совпадение обнаружено: дело в размере, а не в клиенте

Как только стало ясно, что граница проходит по размеру файла, а не по типу клиента, гипотезы резко сузились. Маленькие запросы умещаются в один сетевой пакет и никогда не сталкиваются с фрагментацией — именно поэтому они работали у всех без исключения. Как только тело запроса начинает превышать размер одного пакета (обычно это происходит на границе около 1400–1500 байт полезной нагрузки TCP, в зависимости от накладных расходов туннелей на пути), в игру вступает механизм, о котором в обычной эксплуатации вспоминают редко — Path MTU Discovery.

Дальше стало понятно, почему проблема была именно избирательной, а не тотальной или полностью случайной:

  • У пользователей, чей путь до сервера состоял из стандартных сегментов с MTU 1500 (обычный Ethernet, обычный домашний или офисный интернет без дополнительных туннелей), крупные пакеты проходили по всему маршруту без единого узкого места — фрагментировать было незачем.
  • У пользователей, чей трафик на каком-то участке пути проходил через звено с меньшим MTU — корпоративный VPN-туннель, мобильную сеть с инкапсуляцией на стороне оператора, IPsec- или GRE-туннель у себя дома — full-size пакеты сервера физически не помещались в это звено. Маршрутизатор на границе такого звена должен был либо разбить пакет на части, либо, если в IP-заголовке стоит бит DF (Don't Fragment, а современный TCP-стек ставит его почти всегда для реализации PMTUD), отправить обратно ICMP-сообщение «пакет слишком большой, нужна фрагментация» (это ICMP type 3 code 4 — Destination Unreachable / Fragmentation Needed).

В теории отправитель получает это ICMP-сообщение, снижает размер пакета до указанного в сообщении MTU и повторяет передачу — процесс невидим для пользователя и занимает миллисекунды. На практике это работает только если ICMP-сообщение реально доходит до отправителя. Именно здесь и была найдена причина: у части путей ICMP-ответ терялся, и сервер, отвечающий клиенту большим объёмом данных, просто никогда не узнавал, что пакет нужно уменьшить. Пакет уходил в пустоту раз за разом, TCP пытался ретрансмиссию с тем же размером — и результат был тем же самым: тишина.

Матчасть: MTU, PMTUD и почему ICMP — это не лишняя опция

MTU (Maximum Transmission Unit) — максимальный размер пакета, который может пройти через конкретное сетевое звено без фрагментации. Для классического Ethernet это 1500 байт на уровне IP. Любой туннель — WireGuard, OpenVPN, IPsec, GRE, PPPoE — добавляет свои служебные заголовки поверх исходного пакета, а значит эффективный MTU внутри туннеля всегда меньше 1500: типичные значения — 1420–1460 для большинства VPN-протоколов, иногда ниже для мобильных сетей с инкапсуляцией на стороне оператора.

Path MTU Discovery — это механизм, которым TCP-стек ищет минимальный MTU по всему маршруту от отправителя до получателя, чтобы не платить за фрагментацию пакетов на промежуточных узлах (сама фрагментация работает, но она дорога по ресурсам роутеров и почти везде на магистральных сетях отключена или депрайоритизирована). Работает это так:

  1. Отправитель ставит на исходящих пакетах бит DF (Don't Fragment) и отправляет данные с расчётом на MTU локального интерфейса (обычно 1500).
  2. Если где-то на пути пакет не помещается в MTU звена, роутер на этой границе не имеет права его разбить (бит DF это запрещает) — вместо этого он должен вернуть ICMP type 3 code 4 с указанием реального MTU звена.
  3. Отправитель получает этот ICMP-пакет, запоминает уменьшенный MTU для данного маршрута и с этого момента шлёт пакеты меньшего размера.

Проблема в том, что этот механизм критически зависит от одного условия — ICMP-сообщение должно дойти обратно до отправителя. Если где-то на этом пути (чаще всего — на файрволе самого сервера) стоит правило «блокировать весь ICMP», PMTUD ломается полностью. Большие пакеты просто исчезают без следа — ни ошибки, ни явного разрыва соединения. Это классический сетевой «чёрный ящик» (blackhole): приложение вечно ждёт ответа, который рано или поздно упирается в таймаут более высокого уровня и превращается в то, что пользователь видит как «зависание».

Это не rare edge case, а частая ошибка эксплуатации: блокировка ICMP на файрволе исторически считается «хорошей практикой безопасности» — мол, если сервер не отвечает на ping, его сложнее просканировать. На деле type 3 code 4 не имеет ничего общего с ping (type 8 / type 0, echo request/reply) — это два разных типа ICMP-сообщений с разной ролью, но правила вида iptables -A INPUT -p icmp -j DROP или блокировка ICMP «оптом» в security group режут их оба одним махом.

Диагностика: находим порог через ping с DF-флагом

Когда закономерность по размеру подтвердилась, дальше всё стало вопросом техники, а не догадок. Способ проверить гипотезу — отправить ICMP echo-запросы разного размера с явно выставленным битом DF и найти точный порог, на котором ответы перестают приходить. На Linux это делается так:

# Пробуем разные размеры полезной нагрузки, DF выставлен явно (-M do)
ping -M do -s 1300 -c 3 example.com
ping -M do -s 1400 -c 3 example.com
ping -M do -s 1472 -c 3 example.com

Важный нюанс: -s задаёт размер данных ICMP, реальный размер IP-пакета будет на 28 байт больше (8 байт заголовка ICMP + 20 байт заголовка IP). Поэтому -s 1472 при DF даёт ровно 1500-байтный пакет — стандартный порог для чистого Ethernet без туннелей.

Проверка с рабочих машин пострадавших пользователей показала: пакеты с payload до определённого значения (граница легла в районе 1350–1400 байт, что соответствует MTU туннеля примерно 1380–1420 — характерное значение для клиентского VPN с дополнительной инкапсуляцией) проходили нормально, а как только размер переваливал за эту границу — ответ не приходил вообще, без единого ICMP-сообщения о необходимости фрагментации. Именно отсутствие этого сообщения и было диагностическим признаком: при исправно работающем PMTUD ping с DF на большом пакете вернул бы явный ответ Frag needed and DF set, ... mtu = 1400, и стек сам подстроился бы под меньший размер. Здесь же было полное молчание — прямое указание на файрвол, режущий ICMP где-то на пути.

Для Windows тот же тест выглядит так:

ping -f -l 1400 example.com
ping -f -l 1472 example.com

Флаг -f — это аналог DF, -l задаёт размер payload. Если при увеличении размера ответ пропадает без сообщения Packet needs to be fragmented but DF set is, а просто уходит в таймаут — это тот же самый симптом.

Дополнительно ситуацию подтвердили через tcpdump на сервере: пакеты от клиентов из «проблемной» группы приходили на сервер нормально, а ответные пакеты сервера, превышающие MTU их пути, не порождали встречного ICMP-трафика — резался он не на стороне клиента, а на файрволе сервера, о чём подробнее ниже.

Причина и исправление: ICMP type 3 code 4 плюс MSS clamping

Корневая причина оказалась простой и до обидного типичной: на файрволе сервера стояло общее правило, блокирующее весь входящий ICMP «в целях безопасности» — шаблон, который копируют из гайда в гайд, не разбирая, какие типы ICMP реально нужны для работы сети. Когда клиенты с меньшим MTU (за корпоративным VPN, за мобильным туннелем оператора) пытались получить от сервера пакет большего размера, промежуточный роутер честно генерировал ICMP «нужна фрагментация» в сторону сервера — но сервер этот пакет просто дропал на входе, не разбираясь, что это не ping, а служебное сообщение, без которого TCP не может адекватно работать.

Исправление разбивается на два независимых, но взаимодополняющих шага.

Шаг 1 — не блокировать ICMP полностью, а точечно разрешить нужный тип. Даже если у вас есть требование ограничивать ICMP в целом (например, запрещать входящий ping, чтобы не светить хост в простых сканерах), это не повод резать вообще все типы. На iptables правило будет выглядеть так:

# Разрешаем ICMP "Fragmentation Needed" — критично для PMTUD
iptables -A INPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT
iptables -A OUTPUT -p icmp --icmp-type fragmentation-needed -j ACCEPT

# Остальной ICMP можно продолжать ограничивать по своей политике,
# но это правило должно идти ДО общего DROP

Для nftables то же самое:

table inet filter {
  chain input {
    type filter hook input priority 0;
    icmp type destination-unreachable accept
    # остальные правила ICMP/общая политика ниже
  }
}

Для IPv6 аналог называется по-другому — ICMPv6 type 2 (Packet Too Big), и его блокировка ломает PMTUD ещё жёстче, потому что в IPv6 фрагментация на промежуточных узлах вообще не предусмотрена архитектурой протокола — там PMTUD является единственным механизмом, других вариантов нет:

ip6tables -A INPUT -p icmpv6 --icmpv6-type packet-too-big -j ACCEPT

Шаг 2 — MSS clamping как более надёжная альтернатива, не зависящая от ICMP. Даже после починки правил ICMP остаётся риск: где-то на длинном пути может быть ещё один файрвол (у клиента, у промежуточного провайдера), который вы не контролируете и который тоже режет ICMP. Более надёжный способ — не полагаться на обратную связь через ICMP вообще, а заранее подрезать MSS (Maximum Segment Size) на этапе SYN-пакета. Тогда стороны договариваются о меньшем размере сегмента с самого начала, и большие пакеты, требующие фрагментации, просто никогда не отправляются.

На пограничном роутере или файрволе, через который проходит трафик (особенно актуально, если сервер сам терминирует VPN-туннели или стоит за NAT с туннелированием):

iptables -t mangle -A FORWARDING -p tcp --tcp-flags SYN,RST SYN \
  -j TCPMSS --clamp-mss-to-pmtu

Если сервер не форвардит трафик, а сам является конечной точкой, аналогичное правило ставится в цепочке OUTPUT/POSTROUTING на исходящие SYN-ACK:

iptables -t mangle -A OUTPUT -p tcp --tcp-flags SYN,SYN -j TCPMSS --clamp-mss-to-pmtu

Для WireGuard и большинства современных VPN-решений MSS clamping также можно и нужно настраивать прямо в конфигурации туннельного интерфейса — подробнее про подбор MTU для WireGuard есть отдельный разбор с конкретными значениями и таблицей, если у вас туннель на этом протоколе.

Практический вывод: держите оба слоя защиты одновременно. ICMP fragmentation-needed должен быть разрешён всегда, вне зависимости от общей политики по ICMP — это не про удобство сканирования, а про базовую работоспособность TCP. MSS clamping на границе сети — подстраховка на случай, если где-то вне вашего контроля всё равно найдётся звено, которое режет ICMP. Если у вас уже была похожая история с обрывами без явных ошибок, см. общий разбор MTU и фрагментации и инструкцию по подбору правильного значения MTU для сетей поверх VPN-туннелей.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Почему проблема была именно у половины пользователей, а не у всех?

Потому что зависело не от «качества» интернета, а от того, проходил ли конкретный маршрут через звено с MTU меньше 1500 (VPN, мобильный туннель оператора, дополнительная инкапсуляция). У кого путь был стандартным Ethernet-путём без таких звеньев — там большие пакеты проходили без необходимости фрагментации, и баг никогда не проявлялся.

Почему соединение зависало, а не выдавало явную ошибку?

Потому что пакет, требующий фрагментации, не отбрасывался с явным сигналом «ошибка» — он просто терялся (blackhole), а TCP на таком фоне продолжает молча пытаться ретранслировать данные, полагаясь на таймауты. Явная ошибка появляется только на уровне более высокого таймаута приложения, что выглядит как зависание, а не как явный сбой.

Разве блокировка ping не считается хорошей практикой?

Блокировка ICMP echo request/reply (собственно ping, type 8/0) — отдельный и в целом безопасный для функциональности шаг. Проблема возникает, когда под одну гребёнку «блокировать ICMP» попадает и type 3 code 4 (Fragmentation Needed) — а это уже критичный для TCP сервис-трафик, а не диагностический инструмент злоумышленника.

Достаточно ли одного MSS clamping без разрешения ICMP?

MSS clamping снижает риск на этапе установления TCP-сессии, но не покрывает все сценарии — например, изменение MTU на пути уже после установления соединения. Разумный подход — держать оба механизма одновременно: они закрывают немного разные случаи.

Как быстро проверить, не в этом ли дело у меня, если похожая жалоба уже есть?

Отправьте с проблемной машины ping -M do -s <размер> (Linux/macOS) или ping -f -l <размер> (Windows) с постепенно увеличивающимся размером до сервера и посмотрите, на каком пороге ответ пропадает без сообщения о необходимости фрагментации — это и будет прямым подтверждением проблемы с PMTUD.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →