Два интерфейса и один шлюз: асимметричный маршрут ронял каждый третий пакет
Сервер работал стабильно месяцами, а после того как к нему добавили второй сетевой интерфейс — для приватной сети бэкапов — начались странные потери пакетов. Не постоянные, не полные, а какие-то рваные: то всё летает, то треть запросов подвисает и таймаутится. Мониторинг молчал, ping почти всегда проходил, а пользователи жаловались на рандомные обрывы. Ниже — как это расследовали и почему причина оказалась куда прозаичнее, чем казалось на первый взгляд: банальная асимметричная маршрутизация между двумя интерфейсами одной машины.
Содержание
С чего всё началось
Сервер изначально был обычным — один сетевой интерфейс, один шлюз по умолчанию, маршрутизация никого не беспокоила годами. Задача была рутинная: подключить сервер к внутренней сети для бэкапов, чтобы не гонять тяжёлый трафик резервного копирования через публичный интернет и не платить за исходящий трафик там, где это тарифицируется. Решение — добавить второй сетевой интерфейс, подключить его к приватной сети провайдера (или к отдельному VLAN, тут не так важно), прописать статический IP и продолжать работать.
Технически всё было сделано корректно с точки зрения "добавить интерфейс и назначить ему адрес": ip addr add на новый интерфейс, статический адрес прописан в конфиге сети (netplan/ifupdown/NetworkManager — механизм проблемы одинаковый для любого из них). Бэкапы уходили в приватную сеть исправно. Но примерно в те же дни начали приходить репорты: часть запросов к сервису на сервере обрывается по таймауту, часть проходит нормально.
Первая реакция — списать на "сеть барахлит", проверить нагрузку, посмотреть на графики CPU и памяти. Всё было в норме. Именно этот момент стоит запомнить: если после добавления второго интерфейса на сервере с уже работающей сетью начинаются нестабильные проблемы с доступностью — в первую очередь проверяйте маршрутизацию, а не железо и не нагрузку.
Симптомы: потери, которые не похожи на случайные
Первое, что смущало — потери были не хаотичными в чистом виде, а с довольно стабильной пропорцией. Примерно каждый третий запрос к одному из сервисов на сервере обрывался. Не 1 из 100, не 50 из 100 — стабильно около трети. Такая регулярность почти всегда указывает не на "плохую сеть" (там потери обычно либо единичные и хаотичные, либо массовые при полном отказе канала), а на системную проблему в конфигурации — что-то отбраковывает пакеты по правилу, а не по случайности.
Второй момент: обычный ping с клиентских машин почти всегда проходил без потерь. Это сразу отсекло версию про "провода/канал плохой" — ICMP echo request/reply для простых схем маршрутизации обычно идут по одному пути туда-обратно, тогда как на выбор маршрута для TCP-сессий влияет больше факторов (состояние соединений, connection tracking на файрволах и NAT-устройствах по пути). Похожая картина уже разбиралась в статье про потерю пакетов, которую не видно в ping — там тоже потери проявлялись избирательно, а простая проверка доступности ничего не показывала.
Третье: проблема касалась конкретных TCP-сессий — устанавливалось соединение, шёл обмен данными, а потом ответ от сервера просто не доходил до клиента, и через таймаут TCP-стек клиента считал соединение мёртвым. Со стороны сервера в логах приложения ничего подозрительного не было — обработка запроса завершалась успешно, ответ отправлялся. То есть проблема была где-то между "сервер отправил пакет" и "клиент получил пакет".
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверХронология расследования
День 1. Проверили нагрузку, память, диск — всё в норме. Проверили логи приложения — ошибок обработки нет. Списали на "возможно, сетевой провайдер шалит", открыли тикет в поддержку. Поддержка ответила, что на их стороне всё штатно.
День 2. Начали собирать статистику по потерям — есть ли закономерность по времени суток, типу запроса, размеру ответа. Закономерность по времени не нашлась, но обнаружилось, что теряются преимущественно ответы большего размера (те, что не влезают в один пакет и идут несколькими сегментами) и что проблема усилилась именно после добавления второго интерфейса — сверили с датой в истории изменений конфигурации.
День 3. Поворотный момент: совпадение по времени с добавлением интерфейса указывало на маршрутизацию, а не на "проблемы у провайдера". Посмотрели таблицу маршрутизации:
ip route show
default via 203.0.113.1 dev eth0
192.168.100.0/24 dev eth1 proto kernel scope link src 192.168.100.10
203.0.113.0/24 dev eth0 proto kernel scope link src 203.0.113.10
На первый взгляд всё логично: default route через eth0 (публичный интерфейс), приватная сеть бэкапов через eth1. Но тут и кроется корень проблемы — таблица маршрутизации в Linux по умолчанию одна, и решение о том, через какой интерфейс отправить исходящий пакет, принимается на основе адреса назначения, а не на основе того, через какой интерфейс пришёл входящий пакет. Для любого исходящего трафика, который не попадает в более специфичный маршрут (192.168.100.0/24 или 203.0.113.0/24), пакет уйдёт через интерфейс из default route, независимо от того, откуда пришёл соответствующий запрос.
День 4. Запустили tcpdump одновременно на обоих интерфейсах, чтобы поймать конкретный обрывающийся запрос:
tcpdump -i eth0 -n 'tcp port 8443' -w eth0_capture.pcap
tcpdump -i eth1 -n 'tcp port 8443' -w eth1_capture.pcap
Дальше — воспроизвели проблемный запрос с клиента и сопоставили дампы по временным меткам и номерам последовательности TCP. Картина стала однозначной: SYN и первые пакеты запроса приходили через один интерфейс, а часть ответных пакетов сервера уходила через другой интерфейс, отличный от того, откуда пришёл запрос. Эти "неправильные" по мнению промежуточного оборудования пакеты просто не доходили до клиента.
Методика одновременного захвата и сопоставления пакетов с двух интерфейсов подробнее разобрана в статье про разбор VPN-трафика через tcpdump.
Механизм проблемы: что такое асимметричная маршрутизация
Асимметричная маршрутизация — это ситуация, когда пакет идёт от точки А до точки Б одним маршрутом, а ответ от Б до А возвращается другим. Сама по себе асимметрия не всегда проблема — в сложных сетях с несколькими провайдерами и BGP это нормальная практика. Проблема возникает, когда на пути стоит устройство, которое отслеживает состояние соединений (stateful firewall, NAT-шлюз) или применяет строгую проверку обратного пути (reverse path filtering, RPF, в режиме strict).
Логика такого устройства простая: "запрос для этой сессии пришёл через интерфейс/канал А — значит, и ответ должен идти через А. Если ответ пытается пройти через интерфейс Б — это похоже на подмену адреса источника (спуфинг), и я отброшу такой пакет". Это ровно тот защитный механизм, который призван останавливать IP spoofing, и он работает штатно — но в случае сервера с несколькими интерфейсами и неправильной маршрутизацией он ошибочно принимает легитимный ответ за подозрительный трафик и роняет его.
В Linux есть похожий встроенный механизм — rp_filter (net.ipv4.conf.*.rp_filter), который в строгом режиме (значение 1) тоже отбрасывает входящие пакеты, если ответ на них должен был бы уйти не через тот интерфейс, откуда они пришли. В разбираемом случае проблема была не в ядре сервера (там rp_filter стоял в мягком режиме 2 — loose), а в промежуточном сетевом оборудовании и/или на стороне провайдера, где стояла строгая проверка соответствия входящего и исходящего пути для одной сессии. Итог один и тот же независимо от того, где именно стоит фильтр: "нелогичный" ответный пакет отбрасывается, создавая эффект случайной, но системной потери пакетов.
Почему потери были именно на треть, а не на 100%? Часть сессий (например, короткие запросы, укладывающиеся в один сегмент) проходили нормально, а часть — нет, в зависимости от того, как для конкретного пакета в моменте разрешался выбор маршрута. В сочетании с кэшированием ARP/маршрутов и особенностями конкретного сетевого стека это давало ту "случайную, но регулярную" картину потерь, которая изначально сбивала с толку.
Корневая причина
После добавления второго интерфейса на сервере осталась только одна таблица маршрутизации (main) и один default route. Явная политика "отвечать через тот же интерфейс, откуда пришёл запрос" настроена не была — ни через policy-based routing, ни через отдельные таблицы маршрутизации per-интерфейс. Система для любого пакета, у которого адрес назначения не попадал в локальный подсетевой маршрут, отправляла его через интерфейс из default route (eth0) — независимо от того, что запрос мог прийти через eth1, если у него тоже был путь наружу (например, приватная сеть имела шлюз в интернет или сервер отвечал на запрос, пришедший через второй адрес по правилам NAT/проброса портов у провайдера).
Проще говоря: маршрутизация по умолчанию в Linux — per-destination, а не per-flow и не symmetric-by-design. Ядро не хранит "память" о том, через какой интерфейс пришёл конкретный TCP-поток, чтобы автоматически ответить туда же — если явно не настроена policy routing, все решения принимаются заново для каждого пакета по таблице маршрутов на основе IP назначения. Пока интерфейс один — это незаметно: выбирать не из чего. Как только путей стало больше — предположение "оно само разберётся, куда отвечать" перестало быть верным.
Похожая по духу проблема с ложным ощущением "сеть просто барахлит" разбиралась в статье обновили ядро — и не поднялась сеть: там тоже причина была не в железе и не в проводах, а в изменившемся поведении сетевого стека после, казалось бы, безобидного изменения конфигурации.
Практическое решение: policy-based routing
Правильный способ гарантировать, что ответ всегда уходит через тот же интерфейс, откуда пришёл запрос — policy-based routing: отдельные таблицы маршрутизации для каждого интерфейса и правила выбора таблицы по адресу источника (ip rule), а не только по адресу назначения.
Общая схема для Linux (пример для eth0 — публичный интерфейс с адресом 203.0.113.10 и шлюзом 203.0.113.1, eth1 — приватный интерфейс с адресом 192.168.100.10 и собственным шлюзом 192.168.100.1, если он есть):
# Создаём отдельные таблицы маршрутизации (в /etc/iproute2/rt_tables)
echo "100 rt_eth0" >> /etc/iproute2/rt_tables
echo "200 rt_eth1" >> /etc/iproute2/rt_tables
# Наполняем таблицу для eth0
ip route add 203.0.113.0/24 dev eth0 src 203.0.113.10 table rt_eth0
ip route add default via 203.0.113.1 dev eth0 table rt_eth0
# Наполняем таблицу для eth1
ip route add 192.168.100.0/24 dev eth1 src 192.168.100.10 table rt_eth1
ip route add default via 192.168.100.1 dev eth1 table rt_eth1
# Правила: пакеты с исходным адресом eth0 маршрутизируются по таблице rt_eth0
ip rule add from 203.0.113.10 table rt_eth0
ip rule add from 192.168.100.10 table rt_eth1
Смысл в том, что ip rule добавляет выбор таблицы маршрутизации не только по адресу назначения (как в основной таблице), но и по адресу источника пакета. Ядро формирует ответ на входящий запрос с адресом источника, на который пришёл запрос — а значит, правила выше гарантированно приведут к выбору правильной таблицы и, соответственно, правильного интерфейса и шлюза для ответа, вне зависимости от того, что говорит основная таблица про адрес назначения.
Проверить итоговые правила и таблицы:
ip rule show
ip route show table rt_eth0
ip route show table rt_eth1
Для дистрибутивов с systemd-networkd или netplan те же правила стоит задавать декларативно, чтобы они переживали перезагрузку и не терялись при пересоздании интерфейсов — держать ip rule/ip route только в разовых командах без сохранения в конфиг это отдельная грабля, которая регулярно путается с темой асимметричной маршрутизации на практике.
Если строгий RPF стоит именно на промежуточном оборудовании или у провайдера, а не в ядре сервера — policy routing на сервере это как раз то, что убирает саму причину: если ответ гарантированно уходит через тот же интерфейс, что и запрос, строгий RPF на промежуточном оборудовании больше не видит "подозрительных" пакетов и перестаёт их ронять.
Проверка симметричности после изменений
После применения policy routing критично не поверить на слово, а проверить на конкретных сессиях, что асимметрии больше нет. Схема проверки:
- Запустить
tcpdumpодновременно на обоих интерфейсах с фильтром по интересующему порту/протоколу. - Сделать несколько тестовых запросов с внешнего клиента к сервису на сервере — как через публичный адрес, так и (если применимо) через второй интерфейс.
- Сверить в дампах, что для каждой сессии (одинаковый tuple: адрес источника, адрес назначения, порты) все пакеты — и входящие, и исходящие — идут через один и тот же интерфейс.
- Дополнительно проверить через
ip route get, каким путём ушёл бы пакет для конкретного адреса источника — это быстрый способ смоделировать выбор маршрута без реального трафика:
ip route get 198.51.100.20 from 203.0.113.10
ip route get 198.51.100.20 from 192.168.100.10
Команда покажет, через какой интерфейс и по какому маршруту система отправит пакет для указанной пары адресов источника и назначения — то решение, которое иначе пришлось бы вылавливать через tcpdump постфактум. Более широкий разбор того, как вообще принимается решение о маршруте пакета в Linux, см. в статье как маршрутизируется пакет от дома до США.
Хорошая практика — держать этот тест как часть регламента при любом изменении сетевой конфигурации сервера, а не только при добавлении интерфейса: смена шлюза, алиас на существующем интерфейсе, изменение сетевых настроек в виртуализации (в Proxmox при работе с мостами и VLAN тоже легко получить похожий эффект — разобрано в статье про сеть в Proxmox, мосты, VLAN и пропавший доступ) — всё это точки, где симметричность маршрутизации может незаметно сломаться.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Почему ping почти всегда проходил, а TCP-сессии обрывались?
ICMP echo request/reply в простых схемах обычно идёт по единственному пути и реже задевает специфичные для TCP-состояния правила фильтрации. Кроме того, ICMP-пакеты маленькие и не создают той нагрузки на многосегментные ответы, где асимметрия проявляется чаще.
Достаточно ли поставить net.ipv4.conf.all.rp_filter = 2 на сервере, чтобы решить проблему?
Loose-режим RPF на сервере снижает риск того, что сервер сам будет ронять входящие пакеты из-за асимметрии, но не устраняет причину, если строгая проверка стоит на промежуточном оборудовании или у провайдера — там нужно, чтобы трафик сервера физически уходил по симметричному пути, и это делает именно policy routing.
Можно ли просто убрать default route для второго интерфейса вместо policy routing?
Если второй интерфейс подключён только к локальной приватной сети без выхода в интернет — да, достаточно локального маршрута до подсети без default route. Но если по нему всё же идут ответы на запросы, пришедшие именно туда, без policy routing проблема останется.
Нужно ли настраивать policy routing при одном сетевом интерфейсе?
Нет — выбирать между путями не из чего, асимметричная маршрутизация в этом сценарии невозможна на уровне самого сервера. Вопрос актуален именно при появлении второго интерфейса, второго адреса или подключения к дополнительной сети.
Как быстро понять, что дело именно в асимметричной маршрутизации?
Главный признак — совпадение по времени с изменением сетевой конфигурации и частичные, но не хаотичные потери многосегментных ответов при исправно проходящем ping. Одновременный tcpdump на всех интерфейсах и сверка по TCP-сессиям снимают все сомнения.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →