Что такое conntrack и как таблица соединений кладёт сервер
Сервер вроде бы жив: нагрузка на CPU в норме, память не забита, nginx отвечает на локальный curl. Но часть клиентов не может достучаться — одни подключения проходят, другие просто зависают и обрываются по таймауту, без внятной ошибки в логах. Если картина именно такая — выборочно, без закономерности — велика вероятность, что дело не в приложении, а в таблице conntrack внутри ядра, которая незаметно переполнилась и молча дропает новые пакеты.
Содержание
- Как работает conntrack: зачем ядру помнить каждое соединение
- nf_conntrack_max: жёсткий лимит и что происходит при переполнении
- Три типичных причины, из-за которых таблица переполняется
- Как посмотреть текущее заполнение таблицы
- Как увеличить лимит и не упереться в память
- Таймауты: как забирать соединения из таблицы быстрее
Как работает conntrack: зачем ядру помнить каждое соединение
Conntrack — это подсистема netfilter в ядре Linux, которая отслеживает состояние каждого сетевого соединения, проходящего через хост. Не только TCP с его SYN/ACK/FIN, но и UDP, ICMP — для них тоже заводится запись о «псевдосоединении», хотя протокол сам по себе состояния не имеет.
Зачем это нужно:
- NAT не может работать без учёта состояния. Когда сервер подменяет адрес источника (SNAT/MASQUERADE) или адрес назначения (DNAT, проброс портов), ядру нужно помнить, какому исходному соединению какая подмена соответствует, чтобы ответный пакет попал обратно к нужному клиенту.
- Stateful-файрвол — правила вида
iptables -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTработают именно потому, что ядро уже знает: этот пакет — часть уже разрешённого соединения, а не новый вход, который надо заново гонять через всю цепочку правил. - Разбор сложных протоколов (FTP, некоторые VoIP-стеки) через conntrack-хелперы: helper видит, что внутри управляющего соединения клиент договорился об отдельном порте для данных, и заранее размечает его как RELATED.
Каждая запись в таблице — это, грубо говоря, пятёрка (протокол, IP и порт источника, IP и порт назначения) плюс текущее состояние (NEW, ESTABLISHED, TIME_WAIT и так далее) и таймаут, после которого запись считается устаревшей и подлежит удалению. Посмотреть содержимое таблицы можно утилитой conntrack из пакета conntrack-tools:
apt install conntrack-tools # Debian/Ubuntu
conntrack -L | head -20
conntrack -L | wc -l
Здесь и кроется проблема: таблица — это не абстракция, а вполне конкретная структура фиксированного размера в памяти ядра. И у неё есть потолок.
nf_conntrack_max: жёсткий лимит и что происходит при переполнении
Максимальное число одновременных записей задаётся параметром nf_conntrack_max. Он не бесконечен — выставлен исходя из объёма памяти при загрузке модуля (или заранее рассчитан дистрибутивом) и не растёт сам по себе под нагрузкой.
Как только число активных записей в таблице упирается в этот лимит, происходит следующее: ядро не начинает возвращать клиенту явный отказ (RST, ICMP unreachable), оно просто молча дропает пакет, который должен был создать новую запись в таблице. С точки зрения клиента это выглядит как соединение, которое зависло — SYN ушёл, ответа нет, дальше только таймаут на стороне клиента.
Характерный симптом именно этой проблемы — избирательность: часть новых подключений проходит нормально (те, что попали в момент, когда в таблице было свободное место или старые записи как раз были вычищены по таймауту), часть — нет, причём без видимой закономерности по IP, порту или времени суток. Если бы дело было в файрволе или блокировке по IP, картина была бы стабильной и воспроизводимой. Здесь она "плавает" — то работает, то нет на тех же самых запросах.
Проверить, что дело действительно в этом, можно по логу ядра:
dmesg | grep -i conntrack
# или
journalctl -k | grep -i "table full"
Классическая строка в логе выглядит примерно так: nf_conntrack: table full, dropping packet. Если она есть и повторяется — совпадение с жалобами пользователей на "то работает, то нет" можно считать подтверждённым.
Стоит отдельно отметить: если на сервере вообще не используется NAT и не стоит ни один stateful-файрвол (голый iptables без модуля conntrack, либо nftables без stateful-правил) — таблица может не задействоваться вовсе или использоваться минимально. Но в подавляющем большинстве конфигураций — Docker (который сам поднимает NAT-правила для своих сетей), любой reverse-proxy за NAT, VPN-сервер с пробросом портов — conntrack работает всегда, часто незаметно для администратора, пока не начнутся проблемы.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверТри типичных причины, из-за которых таблица переполняется
1. DDoS-атака или массовое сканирование портов. Каждый SYN-пакет от атакующего — это новая попытка создать запись в таблице (со статусом NEW, ещё не ESTABLISHED). При SYN-флуде или широком сканировании диапазона портов число таких "недосозданных" записей может расти кратно быстрее, чем легитимный трафик успевает их вытеснять по таймауту. Таблица забивается мусорными полу-соединениями, которые никогда не станут ESTABLISHED, а место для настоящих клиентов кончается.
2. Огромное количество мелких, коротких соединений. Это не обязательно атака — иногда это архитектурная особенность нагрузки. Классический случай: keep-alive выключен или настроен слишком агрессивно (например, keepalive_timeout в nginx выставлен в 0 или очень маленький), и клиент вместо одного долгоживущего HTTP-соединения на пачку запросов открывает новое TCP-соединение на каждый чих. Пока каждое из них ещё не закрылось до конца (TCP проходит через TIME_WAIT после закрытия — обычно порядка десятков секунд, конкретное значение зависит от nf_conntrack_tcp_timeout_time_wait в вашей системе), в таблице одновременно висит гораздо больше записей, чем реально "живых" соединений с точки зрения приложения. Похожий эффект дают агрессивные автоматические проверки доступности (health-check каждую секунду с новым соединением вместо переиспользования) и плохо написанные клиенты API, которые не умеют в connection pooling — про правильную настройку пула соединений у нас есть отдельный разбор: connection pooling — зачем нужен и как настроить.
3. Утечка через UDP-соединения без явного закрытия. У UDP нет хендшейка и нет закрытия — протокол просто отправляет датаграммы. Ядро вынуждено эвристически решать, когда "закрыть" такую псевдо-сессию: оно ставит таймаут (обычно короче для одностороннего трафика без ответа — это фиксируется параметром nf_conntrack_udp_timeout, — и длиннее, если ответ был получен и сессия считается "подтверждённой" — nf_conntrack_udp_timeout_stream). Конкретные значения таймаутов в вашей системе стоит проверить командой ниже, а не полагаться на цифры "по умолчанию из интернета" — они отличаются между дистрибутивами и версиями ядра. Проблема возникает, когда приложение открывает много UDP-сокетов (например, VoIP-шлюз, игровой сервер, DNS-резолвер под нагрузкой, WireGuard с большим числом пиров) и не переиспользует их — записи накапливаются быстрее, чем истекают по таймауту.
Как посмотреть текущее заполнение таблицы
Базовая проверка — сравнить текущее число записей с лимитом:
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
То же самое через sysctl (эквивалентно, просто другой интерфейс к тем же значениям):
sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
Если первое число близко ко второму (скажем, счётчик держится в районе 90-100% от максимума) — это уже тревожный сигнал, даже если явных дропов в dmesg пока не было: система работает на пределе, и любой всплеск трафика (легитимный или нет) переполнит таблицу.
Полезно смотреть не только на общее число, но и на структуру — что именно занимает место:
conntrack -L | awk '{print $1}' | sort | uniq -c | sort -rn
Эта команда покажет, сколько записей приходится на tcp, udp, icmp — если внезапно основную массу составляет udp, а на сервере не должно быть тяжёлого UDP-трафика, это указывает на утечку или атаку по UDP, а не на обычную веб-нагрузку.
Для разбора, кто именно генерирует нагрузку, можно отфильтровать записи по конкретному состоянию или адресу:
conntrack -L -p udp | wc -l
conntrack -L | grep 203.0.113.10
Если нужно понять, кто вообще нагружает сервер сетевыми соединениями (не обязательно до стадии переполнения conntrack, а в целом) — у нас есть отдельная статья с инструментами для этого: как узнать, кто нагружает сервер. А если подозрение падает на DDoS или сканирование — начните с первых действий при DDoS-атаке и, для более системной защиты, с материала защита от DDoS на выделенном сервере.
Как увеличить лимит и не упереться в память
Если после проверки видно, что лимит действительно тесен для реальной легитимной нагрузки (а не что таблицу забивает атака, которую надо сначала отсечь на файрволе или у провайдера), лимит можно поднять.
Временное изменение (до перезагрузки):
sysctl -w net.netfilter.nf_conntrack_max=131072
Постоянное — через конфиг sysctl:
# /etc/sysctl.d/99-conntrack.conf
net.netfilter.nf_conntrack_max = 131072
Применить без перезагрузки:
sysctl -p /etc/sysctl.d/99-conntrack.conf
Важный нюанс: nf_conntrack_max — не единственный параметр, влияющий на ёмкость таблицы. Под капотом записи хранятся в хеш-таблице, размер которой (hashsize) задаётся отдельно и напрямую влияет на то, насколько эффективно ядро ищет и вставляет записи при большом их числе:
cat /sys/module/nf_conntrack/parameters/hashsize
Если поднять nf_conntrack_max на порядок, а hashsize оставить прежним, есть риск получить не переполнение, а деградацию производительности из-за длинных цепочек коллизий в хеш-таблице. Менять hashsize можно через запись в этот файл или при загрузке модуля через /etc/modprobe.d/ (параметром nf_conntrack.hashsize=). Конкретное соотношение между hashsize и nf_conntrack_max, которое стоит держать, зависит от версии ядра и дистрибутива — единой пропорции, которую можно взять не глядя, здесь нет, стоит свериться с документацией при существенном увеличении лимита.
Второй момент — память: каждая запись в таблице занимает какое-то количество памяти ядра, и вся эта память резервируется под таблицу, а не под приложения. На сервере с небольшим объёмом RAM бездумно задранный nf_conntrack_max может обернуться нехваткой памяти в неподходящий момент — если у вас уже была ситуация, когда ядро само решает, кого "убить" при нехватке памяти, у нас есть отдельный разбор: как ядро выбирает жертву OOM killer. Разумный подход — поднимать лимит постепенно, смотреть на реальную занятую память и на то, насколько ближе фактическое число записей подошло к новому потолку, а не сразу ставить значение "с запасом на все случаи жизни".
Таймауты: как забирать соединения из таблицы быстрее
Если поднимать лимит бесконечно не хочется (и правильно — это не решает проблему, если причина в утечке или в аномально коротких таймаутах для рабочей нагрузки), вторая ручка — таймауты, после которых запись считается устаревшей и освобождает место.
Основные параметры, которыми можно управлять:
| Параметр sysctl | За что отвечает |
|---|---|
net.netfilter.nf_conntrack_tcp_timeout_established | Как долго держится запись для установленного TCP-соединения без активности |
net.netfilter.nf_conntrack_tcp_timeout_time_wait | Таймаут для TCP-соединений в состоянии TIME_WAIT после закрытия |
net.netfilter.nf_conntrack_udp_timeout | Таймаут для UDP-сессии, на которую ещё не пришёл ответ |
net.netfilter.nf_conntrack_udp_timeout_stream | Таймаут для UDP-сессии, где обмен уже идёт в обе стороны |
net.netfilter.nf_conntrack_generic_timeout | Таймаут для прочих типов трафика, которые conntrack не умеет разбирать детально |
Посмотреть текущие значения на своей системе (именно свои, а не чужие цифры из статьи):
sysctl -a 2>/dev/null | grep nf_conntrack.*timeout
Логика тут простая: если nf_conntrack_tcp_timeout_established выставлен неоправданно большим (в некоторых образах он действительно бывает завышен под задачи, для которых сервер изначально не разворачивался), простаивающие, по сути мёртвые, но формально ещё не закрытые соединения занимают место в таблице намного дольше необходимого. Уменьшение этого таймаута — не универсальное решение (для приложений с реально долгоживущими idle-соединениями, например некоторых WebSocket-сценариев, слишком агрессивное сокращение таймаута начнёт рвать легитimные сессии), но для типичного веб-сервера с обычным HTTP-трафиком разумное снижение освобождает таблицу быстрее.
Если нужно точечно избавиться от конкретных зависших записей прямо сейчас, не трогая настройки глобально, можно удалить их вручную:
conntrack -D -p udp --dst 203.0.113.10
conntrack -D -s 198.51.100.20
Это разовая мера на момент разбора инцидента, а не замена нормальной настройке таймаутов или устранению первопричины (атаки, утечки в приложении или неверных настроек keep-alive).
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Conntrack как-то связан с обычным файрволом ufw/firewalld?
Да, напрямую. И ufw, и firewalld под капотом используют iptables/nftables со stateful-правилами (ESTABLISHED,RELATED), а значит, полагаются на ту же таблицу conntrack. Проблема переполнения проявится одинаково независимо от того, каким инструментом вы управляете правилами файрвола сверху.
Можно ли вообще отключить conntrack, если он не нужен?
Технически да, если на сервере не используется NAT и нет stateful-правил файрвола — например, чисто статические ACCEPT/DROP правила без -m conntrack/-m state. Но на практике почти любой современный стек (Docker, большинство VPN-серверов, reverse-proxy с пробросом портов) так или иначе задействует NAT, и отключение conntrack сломает именно эту функциональность. Отключать стоит только осознанно, проверив, что ничего из вышеперечисленного не используется.
Переполнение conntrack — это то же самое, что нехватка файловых дескрипторов или портов (ephemeral ports)?
Нет, это разные ограничения, хотя симптомы могут быть похожи (соединения не проходят под нагрузкой). Нехватка файловых дескрипторов и исчерпание диапазона локальных портов — это лимиты уровня процесса и стека TCP/IP соответственно, они проверяются и настраиваются отдельно (ulimit, net.ipv4.ip_local_port_range). Стоит проверить оба варианта, а не останавливаться на первом подозрении.
Увеличение nf_conntrack_max — это защита от DDoS?
Нет, это просто способ не падать раньше времени и дать себе пространство для манёвра, пока разбираетесь с источником нагрузки. Против целенаправленной атаки конечный лимит рано или поздно снова будет пробит, если не отсечь мусорный трафик на уровне файрвола, upstream-фильтрации у провайдера или сервиса защиты от DDoS.
Как понять, что именно conntrack, а не что-то другое, стало причиной проблем с подключениями?
Три признака вместе: строка table full в dmesg/journalctl -k, значение nf_conntrack_count близкое к nf_conntrack_max, и характерная "выборочность" симптома — часть подключений проходит, часть нет, без стабильной закономерности по IP или URL.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →