Один клиент открыл 60 тысяч соединений и убил NAT на шлюзе
В один момент у сервера перестают устанавливаться вообще все новые соединения — не у одного клиента, а сразу у всех, кто пытается подключиться заново. При этом CPU простаивает, память свободна, диск не нагружен — по всем обычным метрикам сервер здоров. Именно так выглядела авария, о которой этот разбор: причиной оказался не сбой инфраструктуры и не атака извне, а один-единственный клиент, чьё приложение открыло 60 тысяч соединений и не закрыло ни одного, забив таблицу conntrack на шлюзе под завязку.
Содержание
- Что случилось: сервис "лёг" для всех, хотя ресурсы сервера были в норме
- Первые проверки: приложение, БД, диск — везде чисто
- Conntrack: невидимая таблица, которая тоже конечна
- Как нашли виновника: один IP съел всю таблицу
- Корневая причина: клиент открывал соединения быстрее, чем закрывал
- Что сделали: сначала блокировка, потом системные меры
Что случилось: сервис "лёг" для всех, хотя ресурсы сервера были в норме
Первый сигнал пришёл от мониторинга доступности: одновременно у нескольких разных клиентов, не связанных между собой ни географией, ни типом трафика, начались обрывы при попытке подключиться к серверу. Симптом был предельно простым и от этого пугающим — не "часть запросов работает медленнее", а "новое TCP-соединение либо не устанавливается вообще, либо разрывается почти сразу после SYN". Уже установленные, давно живущие соединения при этом продолжали работать нормально — рвались именно попытки подключиться заново.
Первая реакция дежурного инженера была ожидаемой: посмотреть на нагрузку. Load average, top, free -m — всё выглядело буднично, ни всплеска CPU, ни исчерпания памяти, ни аномалий по диску. Само приложение на уже открытых соединениях отвечало без задержек — процесс не завис и не тормозил обработку. Проблема явно была не в вычислительных ресурсах, а где-то на уровне сети — но и канал не показывал насыщения по трафику.
Важная деталь, указавшая направление поиска: обрывы были не избирательными по клиентам ("у одного региона плохо, у другого хорошо"), а тотальными для новых подключений при этом без связи с нагрузкой на CPU или каналом. Такое поведение почти всегда сигнализирует, что дело не в приложении и не в канале, а в промежуточном слое, который отслеживает состояние соединений: NAT и его таблица conntrack.
Первые проверки: приложение, БД, диск — везде чисто
Прежде чем переходить к сетевому слою, стоило закрыть более очевидные гипотезы, и это заняло около двадцати минут — не потому что они казались вероятными, а чтобы не искать сложную причину там, где могла быть простая.
Проверили логи приложения — ошибок подключения к базе не было, пул соединений не был исчерпан, число активных воркеров не росло аномально. Проверили саму базу — активные запросы и блокировки были в обычных пределах для этого времени суток. Проверили диск — iostat -x 1 не показывал ни высокого %util, ни очередей на запись. Проверили лимиты открытых файловых дескрипторов через cat /proc/<pid>/limits — до потолка ulimit -n было ещё далеко.
Отдельно проверили саму операционную систему на предмет исчерпания локальных ресурсов, которые могли бы объяснить обрывы именно новых соединений:
ss -s
cat /proc/sys/net/ipv4/ip_local_port_range
Диапазон эфемерных портов не был исчерпан, и ss -s показывал вменяемое число сокетов в разных состояниях — не миллионы, не десятки тысяч TIME_WAIT, ничего похожего на классическое исчерпание портов на самом сервере. Это отмело ещё одну распространённую причину массовых обрывов — но не объяснило симптом. К этому моменту стало ясно, что искать нужно не в приложении и не в самой ОС сервера, а в узле, через который проходит трафик до него — на шлюзе с NAT.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверConntrack: невидимая таблица, которая тоже конечна
Дальше стоит сделать шаг назад и напомнить механизм, из-за которого один клиент вообще способен уронить сеть для всех остальных. Любой шлюз, файрвол или NAT-устройство, которое пропускает через себя трафик нескольких машин под одним внешним IP (или просто фильтрует пакеты с учётом состояния — stateful firewall), обязано где-то хранить информацию о каждом активном соединении: кто с кем разговаривает, какой у соединения статус, куда возвращать ответный пакет. Эта информация живёт в таблице отслеживания соединений — в Linux это подсистема conntrack, часть netfilter.
Ключевой факт, который в спокойное время незаметен, а в аварии становится решающим: у этой таблицы есть жёсткий потолок по числу записей. Он задаётся параметром net.netfilter.nf_conntrack_max и физически ограничен объёмом памяти, которую ядро готово выделить под хеш-таблицу и сами записи conntrack. Подробнее о том, как устроен сам механизм NAT и зачем ему вообще нужно что-то запоминать про каждое соединение, — в отдельном разборе как работает NAT и почему устройства не видят друг друга. Посмотреть текущий лимит и текущее заполнение можно так:
cat /proc/sys/net/netfilter/nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_count
На многих серверах и ВМ по умолчанию (если параметр не поднимали осознанно) лимит стоит в районе 65536 записей — величина, когда-то казавшаяся огромным запасом, но с ростом числа keep-alive сессий, WebSocket-подключений и фоновых воркеров переставшая быть недостижимой в трафике одного приложения.
Важно понимать: таблица conntrack общая для всех клиентов, проходящих через шлюз, и по умолчанию не резервирует часть ёмкости под каждого из них отдельно — нет встроенного разделения "этому IP положено не больше N записей". Если таблица заполнена целиком, ядро физически не может создать новую запись ни для кого — не важно, кто её забил: один агрессивный клиент, баг в приложении или тысяча легитимных пользователей сразу. Как только nf_conntrack_count упирается в nf_conntrack_max, новые пакеты, требующие создания записи (первый SYN TCP-соединения, первый пакет UDP-потока), начинают отбрасываться — обычно молча, без вменяемой ошибки у клиента, кроме таймаута или сброса.
Это и есть механизм аварии: не "сервер не справляется с нагрузкой", а "общий ресурс — таблица состояний на шлюзе — оказался физически исчерпан, и стал недоступен всем сразу, хотя исчерпал его один участник". Общая механика того, как выборочные обрывы соединений сигнализируют именно о переполнении этой таблицы, разобрана в статье таблица conntrack переполнилась — соединения рвутся выборочно; в этом инциденте картина была даже более радикальной — рвались не выборочные, а вообще все новые соединения, потому что таблицу занял один клиент почти целиком.
Как нашли виновника: один IP съел всю таблицу
Проверка заполненности таблицы дала однозначный результат — nf_conntrack_count был практически равен nf_conntrack_max, с запасом в считаные сотни записей. Это подтвердило гипотезу: дело не в приложении, не в диске и не в конкретном клиенте с "плохой" сетью, а в переполненной таблице conntrack на шлюзе.
Следующий шаг — понять, кто именно занимает таблицу. Просто посмотреть список всех записей бесполезно, если их десятки тысяч, поэтому нужно агрегировать по адресу источника:
conntrack -L 2>/dev/null | grep -oP 'src=\K[0-9.]+' | sort | uniq -c | sort -rn | head -20
Результат был показательным: подавляющее большинство строк принадлежало одному-единственному IP-адресу. Проверка через conntrack -L -s <IP> и подсчёт записей именно для этого адреса дали цифру около 60 000 одновременных соединений — почти вся ёмкость таблицы на этом шлюзе была занята одним клиентом, тогда как весь остальной легитимный трафик в обычное время занимал заметно меньшую долю от лимита.
Полезная команда для той же диагностики без утилиты conntrack — прямое чтение из /proc, если пакет conntrack-tools не установлен:
cat /proc/net/nf_conntrack | awk '{for(i=1;i<=NF;i++) if($i ~ /^src=/){print $i; break}}' | sort | uniq -c | sort -rn | head -20
Оба способа указывали на один и тот же адрес. Это сняло последний вопрос: проблема не в самой сети провайдера и не в аномалии на стороне шлюза, а в конкретном клиенте, чьё приложение открывало соединения аномально быстро и не закрывало их.
Корневая причина: клиент открывал соединения быстрее, чем закрывал
Когда виновный IP известен, важно не просто заблокировать его, а понять механизм — от этого зависит, повторится ли инцидент. В данном случае клиентское приложение не закрывало соединения после использования: либо из-за бага в логике повторных попыток (retry без корректного close() предыдущего сокета при таймауте или ошибке), либо из-за некорректно настроенного пула, который на каждый неудачный запрос создавал новое соединение вместо переиспользования старого, либо — что тоже нельзя было исключить на старте разбора — из-за намеренной попытки исчерпать ресурсы сервиса.
Отличить "баг клиента" от "атаки" по одной этой таблице сложно — с точки зрения conntrack оба сценария выглядят одинаково: быстрый рост числа записей от одного источника. Разница обычно проявляется в деталях трафика — повторяются ли одни и те же запросы, есть ли user-agent или паттерн, характерный для конкретного клиентского SDK, было ли ранее у этого клиента похожее поведение в логах. В этом случае косвенные признаки (характер запросов, известный клиент из истории аккаунта) указывали на баг в клиентском коде, а не на целенаправленную атаку — но с точки зрения немедленной реакции это не меняло ничего: результат для остальных пользователей был одинаково разрушительным, и первые шаги были бы теми же, что и при первых действиях после DDoS-атаки — сначала остановить деградацию сервиса, потом разбираться в природе источника.
Здесь стоит явно проговорить фундаментальную уязвимость конфигурации, которая сделала инцидент возможным: до этого момента на шлюзе не было никакого ограничения на число одновременных соединений с одного IP-адреса. Таблица conntrack была общим неразделяемым ресурсом, и любой клиент — обычный или сбойный — теоретически мог занять её целиком. То, что до сих пор этого не происходило, было везением, а не защитой.
Что сделали: сначала блокировка, потом системные меры
Немедленное восстановление сервиса для остальных клиентов потребовало простого и быстрого действия — заблокировать проблемный IP на файрволе, чтобы освободить таблицу conntrack:
iptables -I INPUT -s <проблемный_IP> -j DROP
После блокировки записи проблемного клиента продолжали существовать в таблице ещё некоторое время (до истечения тайм-аута состояния — для установленных TCP-соединений это обычно несколько дней по умолчанию, если не сбросить принудительно), поэтому потребовалось явно вычистить именно его записи из conntrack, а не просто ждать:
conntrack -D -s <проблемный_IP>
Сразу после этого nf_conntrack_count резко просел, освободившееся место немедленно занялось нормальными новыми соединениями от остальных клиентов, и сервис восстановился — без перезапуска приложения, без вмешательства в его код, потому что само приложение всё это время работало корректно.
Дальше начались уже не аварийные, а системные изменения — потому что временный DROP по IP решает конкретный инцидент, но не убирает саму возможность его повторения с другого адреса или с этого же после разблокировки.
Первое и самое важное системное изменение — ограничение числа одновременных соединений с одного IP на уровне файрвола, с помощью модуля connlimit:
iptables -A INPUT -p tcp --syn -m connlimit --connlimit-above 200 --connlimit-mask 32 -j REJECT --reject-with tcp-reset
Смысл правила простой: если с одного IP-адреса (маска /32 — то есть именно с одного конкретного адреса, а не с подсети) уже открыто больше 200 новых TCP-соединений, следующие SYN-пакеты с этого адреса отклоняются явно, вместо того чтобы дать им забить таблицу. Порог в 200 подбирается под реальный профиль легитимных клиентов конкретного сервиса — он должен быть заметно выше, чем нужно любому нормальному пользователю, но заметно ниже, чем ёмкость всей таблицы conntrack, чтобы один клиент физически не мог монополизировать общий ресурс. По той же логике — ограничение частоты и числа запросов от одного источника — устроен и rate limiting на уровне веб-сервера, подробнее об этом смежном механизме — в статье как работает rate limiting.
Второе изменение — увеличение самого лимита таблицы с разумным запасом сверх обычной пиковой нагрузки, чтобы даже кратковременный всплеск легитимных подключений (не от одного агрессивного клиента, а просто от роста числа пользователей) не приближал систему к границе:
sysctl -w net.netfilter.nf_conntrack_max=262144
echo "net.netfilter.nf_conntrack_max = 262144" >> /etc/sysctl.d/99-conntrack.conf
Конкретное значение здесь — ориентир, а не рецепт: правильный лимит зависит от объёма памяти сервера (каждая запись conntrack занимает какое-то количество байт ядра, и на серверах с малым RAM бездумно задирать лимит опасно) и от реального профиля нагрузки. Отправная точка — посмотреть, на каком уровне обычно держится nf_conntrack_count в пиковые часы, и заложить запас в несколько раз, а не впритык.
Третье — мониторинг заполненности таблицы как отдельной метрики, а не косвенно через CPU и память, которые в этом инциденте не показали вообще ничего. Простейший вариант — периодически снимать соотношение nf_conntrack_count/nf_conntrack_max и отправлять его в систему мониторинга как процент заполнения с алертом на пороге, скажем, 80%:
#!/bin/bash
COUNT=$(cat /proc/sys/net/netfilter/nf_conntrack_count)
MAX=$(cat /proc/sys/net/netfilter/nf_conntrack_max)
echo "conntrack_usage_percent $((COUNT * 100 / MAX))"
Такой скрипт легко подключить как текстовый коллектор к Prometheus/Node Exporter или как проверку в Zabbix — тогда заполнение таблицы становится видимой метрикой задолго до того, как она упрётся в потолок, и у команды будет время среагировать раньше, чем отвалятся все клиенты сразу.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему при переполнении conntrack не помогает перезапуск приложения?
Потому что приложение ни при чём — проблема находится не в его процессе, а в таблице состояний на сетевом уровне (шлюзе, NAT, файрволе). Перезапуск освобождает ресурсы процесса, но не трогает conntrack, поэтому симптом сохранится, пока таблица заполнена.
Как быстро отличить переполнение conntrack от обычной перегрузки сервера?
Проверьте базовые метрики — CPU, память, диск. Если они в норме, а новые соединения при этом массово рвутся или не устанавливаются (при том что уже открытые сессии работают), это сильный признак именно conntrack, а не нагрузки на вычислительные ресурсы. Дальше подтверждается сравнением nf_conntrack_count и nf_conntrack_max.
Достаточно ли просто увеличить nf_conntrack_max, чтобы такое не повторилось?
Нет. Увеличение лимита отодвигает порог, но не устраняет саму возможность, что один клиент займёт всё доступное место — просто теперь ему потребуется больше соединений. Без connlimit (или аналогичного ограничения на число соединений с одного IP) один достаточно агрессивный клиент рано или поздно снова заполнит таблицу целиком, даже увеличенную.
Правило connlimit может заблокировать легитимного клиента с большим числом одновременных подключений?
Может, если порог выбран слишком низко для реального профиля нагрузки. Перед включением правила стоит посмотреть на распределение числа соединений по IP в спокойное время (той же командой conntrack -L | grep -oP 'src=\K[0-9.]+' | sort | uniq -c | sort -rn) и выставить порог с запасом над максимумом, который показывают обычные, не аномальные клиенты.
Нужно ли одновременно чистить таблицу conntrack (conntrack -D) после блокировки IP файрволом?
Да, если нужно восстановить сервис немедленно. Правило DROP или REJECT останавливает только новые пакеты с этого адреса, но уже существующие записи в таблице продолжают занимать место до истечения собственного тайм-аута — иногда это часы или дни. Явная очистка через conntrack -D -s <IP> освобождает место сразу.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →