Зацепка пришла из чужого комментария: кто-то назвал правило --state ESTABLISHED,RELATED бессрочным кредитом доверия и бросил рядом число — 432 000 секунд. Столько Linux по умолчанию держит запись об established-соединении. Это ровно пять суток.
Комментарий не врал, но и не договаривал. Правило действительно выдаёт кредит: пакет, попавший в уже известный поток, дальше не проверяется никакими правилами вообще. Вопрос только в сроке. Пять суток — это больше, чем живёт большинство токенов аутентификации, VPN-сессий и рабочих смен администратора. Мне стало интересно, откуда взялась эта цифра и кто её вообще выбирал.
Дальше выяснилось, что обосновали эту цифру одной фразой про длинный уикенд, и что она — не про безопасность, а про то, сколько стоит ошибка в обе стороны.
Значение живёт в исходниках ядра буквально константой из четырёх слов: [TCP_CONNTRACK_ESTABLISHED] = 5 DAYS. В современном nf_conntrack_proto_tcp.c рядом с таблицей таймаутов до сих пор висит комментарий, оставленный ещё в ветке 2.4: «Надо бы пересмотреть таймауты и переходы состояний в ipfilter внимательнее. Они сложнее. —RR». Инициалы принадлежат Расти Расселлу, а сам комментарий — честная расписка в том, что таблица здесь не результат проектирования, а результат переноса.
Переносить было откуда. Механизм пришёл из двух источников, и оба были написаны для NAT, а не для файрвола. Модуль ip_masq — маскарадинг эпохи ядра 2.0 — держал TCP-запись 15 минут (MASQUERADE_EXPIRE_TCP 15*60*HZ), потому что ему нужно было только помнить трансляцию адресов. А вот IP Filter, откуда пришла сама идея таблицы состояний, держал established-запись ровно пять суток: в описании ipf.5 так и написано — соединение может простоять пять дней, прежде чем его вынесут из таблицы состояний. Пятисуточный таймаут приехал в Linux из мира stateful packet filtering и остался даже тогда, когда оттуда ушёл сам IP Filter.
Один из авторов netfilter объяснил смысл этой цифры в 2005 году фразой, которую сложно улучшить: таймаут выставили таким длинным, чтобы уйти на длинный уикенд и, вернувшись, продолжить работу в той же telnet-сессии. В той же ветке рассылки инженеры обсуждали, что при десятиминутном таймауте придётся не забывать отвечать RST на незнакомый трафик, иначе удалённые клиенты будут тихо «неметь» на живых соединениях.
У этой цифры есть формальный антипод — и он появился спустя семь лет. RFC 5382, написанный в 2008 году для NAT-устройств, требует, чтобы «established connection idle-timeout» был не меньше 2 часов 4 минут. Обоснование в стандарте арифметическое: TCP keep-alive по умолчанию отправляется раз в два часа (это ещё RFC 1122), а четыре минуты добавлены на то, чтобы пакеты успели пересечь устройство. Если держать запись меньше, NAT начнёт рвать живые сессии.
Дефолт Linux превышает этот минимум в 58 раз. И это не по недосмотру: у стандарта зафиксирован пол, но не потолок, потому что потолка у живучести TCP-сессии не существует — established-соединение может простоять бесконечно, не отправив ни одного байта. Стандарт защищал приложения от слишком короткого таймаута; от слишком длинного защищать никого не просили.
Исключения всё-таки есть, и они видны по громкости обсуждений. В 2014 году в OpenWrt завели тикет «значение слишком низкое и не соответствует RFC 5382» — при тогдашних 3600 секундах. Автор тикета просил выставить 7204, а сопровождающий проекта Феликс Фиеткау в коммите поставил 7440 — те самые два часа четыре минуты. Именно это значение OpenWrt везёт до сих пор, и это делает его одним из немногих дистрибутивов, где на роутере таймаут короче, чем на сервере в сто раз.
Ровно та же цифра всплыла в 2026 году в совсем другой истории. На форуме Turris выложили разбор: после обновления система поставлялась со значением 432000 — патч, приводивший таймаут к «правильным» 7440, случайно потерялся при сборке. Дальше описывался совершенно бытовой сценарий: на LAN-машине крутится BitTorrent, таблица роутера рассчитана примерно на 31 232 записи (формула от 512 МБ памяти), и она забивается за часы. После этого ядро начинает возвращать EPERM на любые новые соединения — с любого устройства в сети. Автор разбора честно поправился постфактум: он сначала обвинил конкретный коммит 2020 года, потом сам же признал, что коммит был рефакторингом, а логика пришла из 2018-го, и что его собственный разбор был построен на непроверенной догадке.
Прежде чем считать пять суток безумием, стоит понять, чем разработчику грозит ошибка в каждую сторону. Запись conntrack занимает около 304 байт, плюс потери на выравнивание в slab-аллокаторе — на реальной машине замер давал около 316 байт на поток. Ошибка в сторону «слишком долго» стоит триста байт ОЗУ за каждое лишнее соединение. Ошибка в сторону «слишком коротко» рвёт сессию у живого пользователя: браузер, SSH, база данных, что угодно, что молчало дольше таймаута.
Асимметрия здесь абсолютная, и выбор сделан именно в её пользу. Размер таблицы ядро вообще не назначает явно: nf_conntrack_buckets вычисляется делением всей памяти на 16384 с потолком в 65536 корзин, а nf_conntrack_max по умолчанию равен четырём корзинам. На любой машине с более чем 4 ГБ ОЗУ это даёт 65536 корзин и потолок 262 144 записи — число, которое никто в вашей команде не выбирал. Его выбрала эвристика, написанная для обычного сервера и ничего не знающей о том, что вы собираетесь посадить за один masquerade-правило несколько сотен гостей.
Механизм эвикции тоже устроен не в пользу аккуратности. Когда место кончается, ядро не разбирается, какие записи важнее: функция early_drop перебирает корзину и пропускает всё, у чего выставлен флаг ASSURED, а ASSURED выставляется как раз тогда, когда в established-состоянии увиден корректный ACK. То есть под нож первыми идут записи, созданные по ошибке или по недосмотру; те самые пятисуточные защищены. Это ровно та логика, ради которой цифру и выбирали, и она работает.
Дальше начинается самый неприятный сценарий, и он описан подробно. В исследованиях Cloudflare уменьшили таблицу до семи записей и показали, что происходит при переполнении: SYN-пакеты исчезают в никуда. Ни правила -j DROP, ни переключателя — просто сам факт использования conntrack означает, что при заполненной таблице пакеты, создающие новый поток, отбрасываются жёстко, с единственной строчкой в dmesg: nf_conntrack: table full, dropping packet. Для UDP есть бонус: новые исходящие потоки получают EPERM, то есть приложению возвращается ошибка разрешений там, где кончилось совсем другое.
Классический сюжет на эту тему — история из декабря 2023 года про кластер на GKE. Началось с ошибок разрешения имён и повышенных задержек к «тяжёлым» RPC-сервисам, выглядело как проблема платформы, и автору пришлось идти в поддержку Google. Ответ был скучным: на узлах сыпалось то же самое сообщение про переполненную таблицу. Победить это можно двумя способами — расширить таблицу или сократить число соединений, и автор выбрал второе, потому что менять размер таблицы на продакшене ему не хотелось.
Крупные облачные NAT решают ту же проблему прямо противоположным способом, и делают это цинично и правильно. Шлюз AWS держит неактивное соединение всего 350 секунд и при истечении посылает RST, а не FIN, — то есть не делает вид, что соединение закрыто корректно.
Потолок одновременных соединений к одному адресу назначения — 55 000 на один IP, лечится добавлением адресов. В 2023 году Amazon сделал таймауты конфигурируемыми и в анонсе честно объяснил, зачем: «некоторые нагрузки неэффективно расходуют своё соединение-аллоуэнс из-за утечек и простаивающих сессий». В том же тексте про выбор дефолтов есть замечательное признание, что почти все состояния живут от 30 до 180 секунд, «за исключением established, которое может длиться до пяти дней». Провайдер экономит, потому что платит за метрику; владелец сервера экономит, потому что платит за недовольство пользователей.
А теперь то, ради чего весь этот сюжет вообще стоит разбирать. Пока запись жива, она не перепроверяет ничего: ни пароль, ни сертификат, ни сессию на уровне приложения. И проверяет она не всё, что стоило бы.
В октябре 2024 года исследователи из Anvil Secure описали то, что находили на реальных аудитах, а не в теории. Модуль conntrack не хранит, по какому интерфейсу было установлено соединение. Правило «разрешить RELATED,ESTABLISHED» на внешнем интерфейсе применяется ко всем потокам, включая те, что установлены между двумя внутренними хостами.
Подделать внутренний адрес, отправить пакет с той же четвёркой адрес-порт на публичный интерфейс — и файрвол пропустит его по состоянию. Работает это даже при выключенной маршрутизации между интерфейсами. Дальше авторы спуфингом создавали динамические пробросы портов через NAT-PMP/PCP, отправляли ложные ответы mDNS и вклинивались в обмен между двумя внутренними машинами. В их списке целей — не только роутеры, но и гипервизоры с виртуальными сетями, VPN-шлюзы, встроенные системы и дроны: любое устройство с несколькими интерфейсами, у которого нет anti-spoofing-правил, по их формулировке, «вероятно уязвимо».
Второй сюжет устроен красивее. В 2012 году ядро честно реализовало RFC 5961 — механизм challenge ACK, придуманный как защита от инъекции поддельных пакетов в соединение. Ответ на подозрительный пакет отправляется, но требует точного номера последовательности, а чтобы этот механизм самого не превратили в оружие, частота challenge-ACK ограничена 100 в секунду на всю машину. Глобальным счётчиком.
В 2016 году команда из Калифорнийского университета в Риверсайде показала, что этот счётчик — идеальный канал утечки: атакующий со стороны создаёт нагрузку на общий ресурс и по времени реакции измеряет, разговаривают ли между собой два произвольных хоста и с какими номерами последовательности. Десять секунд на проверку гипотезы, от 40 до 60 секунд на полный цикл, успешность 88–97% — против SSH и Tor, включая разрыв соединения и инъекцию содержимого. Уязвимости дали номер CVE-2016-5696, а исправление в ядре 4.7 (предложенное Линусом) выглядит ровно как ослабление защиты: лимит подняли со 100 до 1000 и добавили рандомизацию, чтобы по счётчику нельзя было считать. Red Hat в своих рекомендациях пошёл до конца: предлагал выставить tcp_challenge_ack_limit в 999999999, то есть эффективно отключить rate limiting, лишь бы закрыть побочный канал.
Хороших мишеней у «кредита по состоянию» хватает и без экзотики. Nat Slipstreaming Сэмми Камкара (2020) заставлял ничего не подозревающего пользователя открыть сайт, который через ALG для SIP создавал на его же роутере новую запись в таблице состояний — и после этого внешний атакующий подключался к внутреннему сервису напрямую. А в 2001 году текст advisory начинался с фразы «почти 100%»: модуль ip_conntrack_ftp разбирал команду PORT и не проверял переданные в ней адрес и порт, вставляя их в таблицу RELATED как есть. Файрвол превращался в allowlist, который заполнял сам атакующий.
Три места, где источники не сходятся и я не буду делать вид, что сходятся.
Первое — про «пять дней» как общее место для stateful-фильтров. В man-странице IPFilter под NetBSD текст говорит прямым предложением: established-запись может простоять пять дней. А таблица переменных в той же man-странице приводит дефолт tcp_idle_timeout как 864000 секунд, то есть десять дней. Одна страница, два числа, отличающиеся вдвое, и ни слова о том, какое живое.
Второе — цифра бывает не только узаконена, но и найдена случайно. Патч, ограничивающий таймаут для связи, «подхваченной» с середины (nf_conntrack_tcp_loose=1 — дефолт, который позволяет одиночному ACK без SYN создать новую запись сразу в established с полным пятисуточным таймаутом), появился в 2013 году после разбора трассы на чьём-то рабочем файрволе. Там после 61-секундной паузы последний ACK породил свежую запись с таймаутом 432000 и флагом UNREPLIED, и автор патча пишет, что «через пару часов таблица начинает забиваться этим мусором». Мне это кажется самой недооценённой строчкой во всей истории: пятисуточная запись может возникнуть из одного пакета, которому никто не отвечал.
Третье — про меня, а не про ядро. Соблазн сказать «файрвол обманывает пользователя» я считаю натяжкой. Таймаут выбирали для того, чтобы не рвать чужую работу, и на своей задаче он вполне оптимален.
Именно поэтому мне не хочется выводить из этой истории мораль про безалаберных системных администраторов. Настоящая уязвимость во всех описанных случаях лежит не в длине кредита, а в природе записи: это четыре числа, описывающие поток, и ничего сверх них. Удлините или укоротите таймаут — эта часть не изменится.
Заглянул заодно в машину под рукой: значение 432000 на месте, потолок таблицы 262 144 записи — числа, которых никто не выбирал, а выдала их эвристика по объёму памяти при загрузке модуля. Держать в голове полезно ровно одно: этот таймаут почти никогда не настраивали осознанно, ни в ту, ни в другую сторону. Его просто унаследовали вместе с кодом.
nf_conntrack_tcp_timeout_established = 432000)net/netfilter/nf_conntrack_proto_tcp.c — исходный код ядра Linux, ветка master (таблица tcp_timeouts, флаг ASSURED, обработка mid-stream pickup)net/ipv4/netfilter/ip_conntrack_proto_tcp.c — архив ядра 2.4 на kernel.org (5 DAYS и комментарий с инициалами --RR)include/net/ip_masq.h и net/ipv4/ip_masq.c — архив исходников ядра 2.x (MASQUERADE_EXPIRE_TCP = 15 минут)tcp_idle_timeout = 8640008b62e7b (Felix Fietkau) — base-files: adjust the default netfilter tcp established connection timeout as per RFC 5382package/kernel/linux/files/sysctl-nf-conntrack.conf — репозиторий OpenWrt (значение 7440)75ff39c в ядре; advisory Red Hat по challenge ACK