АйТи Фреш
Главная / Статьи / Сети
Сети

Пакет виден в tcpdump, но Linux его отбрасывает: разбираем rp_filter

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~20 мин чтения
Пакет виден в tcpdump, но Linux его отбрасывает: разбираем rp_filter
Иллюстрация к статье «Пакет виден в tcpdump, но Linux его отбрасывает: разбираем rp_filter».

На второй сетевой карте виден входящий SYN, сервис слушает порт, а правила firewall разрешают соединение. Ответа всё равно нет. Я, Семёнов Евгений Сергеевич, чаще всего начинаю не с очередного просмотра nftables, а с проверки обратного маршрута и `rp_filter`. Ниже покажу, где именно исчезает пакет, как доказать причину и почему я предпочитаю исправлять маршрутизацию, а не навсегда отключать защиту ядра.

tcpdump подтверждает приём кадра, но не доставку пакета

Главная ловушка — считать строку в tcpdump доказательством того, что пакет прошёл сетевой стек Linux. tcpdump использует пакетный сокет AF_PACKET и получает копию входящего трафика на канальном уровне до передачи пакета протоколам ядра. Поэтому утилита честно показывает SYN, который сетевая карта уже приняла, но который IPv4-стек через несколько шагов признает недопустимым. tcpdump здесь не врёт. Просто администратор приписывает его выводу больше смысла, чем в нём есть.

Для локального IPv4-трафика упрощённый путь выглядит так: драйвер сетевой карты → AF_PACKET, где пакет видит tcpdump → Netfilter PREROUTING → поиск входного маршрута и проверка источника → Netfilter INPUT → сокет приложения. Это важная точность: rp_filter срабатывает не «до любого firewall». Правила и счётчики в PREROUTING могут увидеть пакет, а цепочка INPUT и приложение — уже нет. Поэтому отсутствие события в INPUT при наличии пакета на интерфейсе является хорошей подсказкой.

Я проверяю одновременно три точки. На интерфейсе смотрю SYN через tcpdump, в INPUT — изменение счётчика подходящего правила, а на сервере — наличие слушающего сокета. Команды для нашего примера такие:

sudo tcpdump -ni enp2s0 'host 192.0.2.44 and tcp dst port 443'
sudo nft list ruleset -a
sudo ss -lntp 'sport = :443'

Если SYN есть в первой команде, Nginx слушает 0.0.0.0:443, но счётчик разрешающего правила INPUT не растёт, перестаю перебирать порты и начинаю проверять FIB и reverse path validation.

Не делайте вывод «firewall пропустил пакет» только по захвату на `any` или физическом интерфейсе. Захват и локальная доставка находятся в разных точках сетевого пути.
Памятка: tcpdump подтверждает приём кадра, но не доставку пакета — схема
Памятка: tcpdump подтверждает приём кадра, но не доставку пакета. Открыть схему в полном размере

Что на самом деле проверяет rp_filter

rp_filter — это IPv4-проверка обратного пути по таблицам маршрутизации ядра. Linux берёт адрес источника входящего пакета и выполняет обратный FIB lookup. В строгом режиме результат должен указывать на тот же интерфейс, через который пакет пришёл. Допустим, запрос от 192.0.2.44 вошёл через enp2s0, а лучший маршрут к 192.0.2.44 проходит через основной default route на enp1s0. Для режима 1 это несоответствие. Пакет отбрасывается ещё до INPUT, хотя прямой маршрут к локальному адресу и правило firewall совершенно исправны.

Значения простые: 0 отключает проверку источника, 1 включает строгий режим, 2 — свободный, или loose mode. В loose mode достаточно, чтобы источник считался достижимым хотя бы через один интерфейс. Документация ядра рекомендует строгий режим для обычной маршрутизации и loose mode при предусмотренной асимметрии. Но не надо приписывать значению 2 магическую защиту: если в таблице есть обычный маршрут default, почти любой глобальный unicast-адрес уже считается достижимым.

Есть неприятная особенность, на которой я регулярно ловлю даже опытных администраторов. Для входного интерфейса ядро использует максимальное значение из net.ipv4.conf.all.rp_filter и net.ipv4.conf.<интерфейс>.rp_filter. Если all=1, а на карте выставили 0, проверка фактически остаётся строгой. Параметр default — это шаблон для создаваемых позднее интерфейсов, а не универсальный переключатель уже существующих карт. Поэтому я всегда читаю все уровни сразу:

sysctl net.ipv4.conf.all.rp_filter
sysctl net.ipv4.conf.default.rp_filter
sysctl net.ipv4.conf.enp1s0.rp_filter
sysctl net.ipv4.conf.enp2s0.rp_filter

Имена интерфейсов, конечно, подставляйте свои.

В проверке участвует не только таблица main. Ядро использует Routing Policy Database: правила ip rule, отдельные таблицы и, при соответствующей настройке, метку пакета. Именно поэтому вывод одного ip route недостаточен. ip route show перечисляет записи, а ip route get выполняет разрешение маршрута с заданными атрибутами почти так, как его видит ядро.

Проверяйте эффективную пару `all` плюс конкретный интерфейс. Запись `rp_filter=0` только на сетевой карте ничего не отключит, если глобальное значение равно `1` или `2`.
Пакет виден в tcpdump, но Linux его отбрасывает: разбираем rp_filter — схема
Схема к статье. Открыть схему в полном размере

Практика: две линии у «Актив-Эксперта» и пропавший HTTPS

Покажу случай компании «Актив-Эксперт» — финансовое консультирование, 24 рабочих места. Название условное, топология и цифры сохранены без данных, позволяющих определить организацию. На виртуальном сервере было 2 vCPU, 4 Гбайт RAM и две VirtIO-карты по 1 Гбит/с. Система — Ubuntu Server 24.04.1 LTS, ядро 6.8.0-55-generic, Nginx 1.24.0. Через сервер публиковались клиентский портал для обмена отчётностью и API, через который учётная система клиентов забирала подготовленные документы.

Первая линия была основной: enp1s0, адрес 198.51.100.10/24, шлюз 198.51.100.1, метрика default route — 100. Вторая использовалась для отдельного DNS-имени API: enp2s0, адрес 203.0.113.10/24, шлюз 203.0.113.1, метрика — 200. Адреса здесь заменены на документальные диапазоны RFC 5737. Оба провайдера доставляли трафик до своих адресов, но в таблице main лучший маршрут в интернет всегда проходил через enp1s0. Это нормально для исходящих соединений, но не для ответа на запрос, пришедший через вторую линию.

После усиления sysctl на обеих картах оказался строгий rp_filter=1. Внешняя проверка с адреса 192.0.2.44 отправляла SYN на 203.0.113.10:443. За минуту мы увидели в tcpdump 30 повторных SYN и ни одного SYN-ACK. ss подтверждал слушающий порт, конфигурация Nginx проходила nginx -t, разрешающее правило nftables было на месте, но его счётчик в INPUT не изменился. В access log и error log Nginx — пусто. Именно в этот момент обычно начинают без пользы переустанавливать сертификаты или переставлять правило firewall выше.

Обратный lookup сразу показал конфликт:

ip -4 rule show
ip -4 route show table main
ip -4 route get 192.0.2.44 from 203.0.113.10
ip -4 route get 203.0.113.10 from 192.0.2.44 iif enp2s0

Предпоследняя команда моделирует ответ с адреса второй линии и до исправления выбирала via 198.51.100.1 dev enp1s0. Последняя моделирует сам входящий SYN, включая проверку источника, и честно возвращала RTNETLINK answers: Invalid cross-device link — так ip показывает отказ reverse path filter. Реальный пакет пришёл на enp2s0, а обратный маршрут указывал на enp1s0, и ядро выбрасывало пакет. Маршруты выглядели аккуратно, firewall был правильным, а сервис не получал даже первого SYN.

Диапазоны `192.0.2.0/24`, `198.51.100.0/24` и `203.0.113.0/24` приведены только как безопасные примеры. Не копируйте их в рабочую сеть.
Цифры и версии: Практика: две линии у «Актив-Эксперта» и пропавший HTTPS — схема
Цифры и версии: Практика: две линии у «Актив-Эксперта» и пропавший HTTPS. Открыть схему в полном размере

Как доказать причину, а не угадать её

Сначала я включаю логирование martian-пакетов и повторяю один контролируемый запрос. Настройка log_martians также учитывает глобальное и интерфейсное значения: логирование включено, если хотя бы одно из них равно единице. Сообщения ядра ограничиваются по частоте, поэтому число строк не обязано совпасть с числом SYN.

sudo sysctl -w net.ipv4.conf.all.log_martians=1
sudo journalctl -kf

В нашем случае ядро 6.8 записало martian source 203.0.113.10 from 192.0.2.44, on dev enp2s0 и следом строку ll header с канальным заголовком: первым идёт адрес назначения, после from — источник. В свежих ядрах формат переписан на martian source (src=192.0.2.44, dst=203.0.113.10, dev=enp2s0), поэтому grep по журналу лучше строить по словам martian source, а не по порядку полей. Параллельно смотрю счётчик отбрасываний по обратному пути — он растёт независимо от log_martians и rate limit:

nstat -az TcpExtIPReversePathFilter

Если значение увеличивается синхронно с повторами SYN, причина доказана без чтения журнала.

Затем провожу короткий тест с loose mode только на проблемной карте. Поскольку глобальное значение участвовало в вычислении максимума, его пришлось обнулить, сохранив строгий режим на первой карте:

sudo sysctl -w net.ipv4.conf.all.rp_filter=0
sudo sysctl -w net.ipv4.conf.enp1s0.rp_filter=1
sudo sysctl -w net.ipv4.conf.enp2s0.rp_filter=2

HTTPS сразу ответил. Это диагностический эксперимент, а не мой окончательный проект. После теста я вернул enp2s0 в режим 1 и исправил policy routing. Если вы работаете удалённо, заранее держите вторую консоль или доступ через гипервизор.

Иду всегда в одном порядке: фиксирую входящий интерфейс и адреса пакета; читаю все значения rp_filter; смотрю ip rule; проверяю таблицы командой ip -4 route show table all; моделирую входящий пакет командой ip route get <локальный адрес> from <удалённый адрес> iif <карта> и ответ — командой ip route get <удалённый адрес> from <локальный адрес>; только потом меняю sysctl. Простая команда ip route get 192.0.2.44 хуже: она не передаёт ни локальный адрес второй линии, ни входной интерфейс, поэтому при policy routing проверяет не тот сценарий. Важно не перепутать направление: from с локальным адресом вместе с iif описывает пакет, который якобы пришёл снаружи с нашим же адресом, и ядро отвергнет его по совсем другой причине.

Не считаю conntrack решающим свидетелем в такой диагностике. Connection tracking обрабатывает пакет в PREROUTING, до поиска маршрута, поэтому правила с ct state new в этой цепочке срабатывают и для SYN, который потом отбросит rp_filter. Запись при этом остаётся неподтверждённой и в conntrack -L не появляется — отсутствие записи тоже ничего не доказывает. Надёжная связка здесь — захват на конкретной карте, отсутствие INPUT, результат обратного lookup и martian-сообщение ядра.

Не применяйте постоянные сетевые изменения по единственной SSH-сессии без out-of-band-доступа. Для Netplan используйте `netplan try`: при потере подтверждения конфигурация автоматически откатится.

Моё решение: отдельная таблица для каждого исходного адреса

Если асимметрия возникла случайно из-за двух default route, я не маскирую её значением rp_filter=0. Мой выбор — source-based policy routing: ответ с адреса каждой линии должен уходить через шлюз этой же линии. Это одновременно удовлетворяет строгой обратной проверке, сохраняет предсказуемый путь ответов и не заставляет надеяться, что второй провайдер пропустит пакет с чужим исходным адресом. Кроме того, диагностика через полгода остаётся понятной.

Для «Актив-Эксперта» мы создали таблицы 101 и 102. В каждую положили подключённую сеть и свой default route, а правила привязали к исходным адресам. Постоянная конфигурация Netplan выглядела так:

network:
  version: 2
  renderer: networkd
  ethernets:
    enp1s0:
      addresses:
        - 198.51.100.10/24
      routes:
        - to: default
          via: 198.51.100.1
          metric: 100
        - to: 198.51.100.0/24
          scope: link
          table: 101
        - to: default
          via: 198.51.100.1
          table: 101
      routing-policy:
        - from: 198.51.100.10/32
          table: 101
          priority: 101
    enp2s0:
      addresses:
        - 203.0.113.10/24
      routes:
        - to: default
          via: 203.0.113.1
          metric: 200
        - to: 203.0.113.0/24
          scope: link
          table: 102
        - to: default
          via: 203.0.113.1
          table: 102
      routing-policy:
        - from: 203.0.113.10/32
          table: 102
          priority: 102

Числовые таблицы удобны: ради двух записей не требуется редактировать rt_tables. Приоритеты задаю явно, потому что меньший номер правила обрабатывается раньше.

Конфигурацию мы проверили и применили с автоматическим откатом:

sudo netplan generate
sudo netplan try --timeout 120
ip -4 rule show
ip -4 route show table 101
ip -4 route show table 102
ip -4 route get 192.0.2.44 from 203.0.113.10
ip -4 route get 203.0.113.10 from 192.0.2.44 iif enp2s0

После изменения обратный lookup показывал таблицу 102, шлюз 203.0.113.1 и enp2s0, а модель входящего пакета вместо ошибки возвращала local 203.0.113.10 ... dev lo, то есть локальную доставку. Строгий rp_filter=1 перестал отбрасывать запрос, а SYN-ACK пошёл через того же провайдера. Важно проверить не только подключение к серверу, но и оба направления с двух внешних точек: локальный тест с самого сервера входной путь не воспроизводит.

За первые сутки мониторинг выполнил 1440 HTTPS-проверок через каждую линию — 2880 запросов без потерь. За следующую неделю через вторую линию прошло 6 912 обращений к API; событий martian source для разрешённых внешних адресов и ответов через неверную карту не было. Работы вместе с диагностикой заняли 47 минут — для компании на 24 места это один вечер без простоя портала в рабочие часы. Главное, что мы не просто «разрешили пакет», а убрали случайную асимметрию, из-за которой позже ломались бы ответы, stateful firewall провайдера и анализ инцидентов.

Таблица policy routing должна содержать не только default route, но и маршрут к подсети своего шлюза. Иначе конфигурация может не примениться или шлюз окажется недостижимым в этой таблице.

Откуда берётся rp_filter=2 после установки: дефолты дистрибутивов и sysctl.d

Частый вопрос после разбора: «Мы ничего не включали, откуда строгий или loose-режим?». В самом ядре rp_filter по умолчанию равен 0, и так написано в ip-sysctl.rst. Всё остальное делают пакеты дистрибутива. systemd поставляет /usr/lib/sysctl.d/50-default.conf, где есть три строки: net.ipv4.conf.default.rp_filter = 2, net.ipv4.conf.*.rp_filter = 2 и -net.ipv4.conf.all.rp_filter. Последняя строка без знака равенства не задаёт значение, а исключает all из маски — поэтому глобальный параметр остаётся ядерным 0, а каждая карта получает 2. В Ubuntu поверх этого procps кладёт 10-network-security.conf с 2 и для all, и для default; до Ubuntu 20.04 там стояла единица. В Debian 13 файл 50-default.conf переехал в пакет linux-sysctl-defaults, который ставится по рекомендации systemd, а /etc/sysctl.conf systemd-sysctl больше не читает. RHEL 9 добавляет 50-redhat.conf с default.rp_filter = 1, и на свежей системе легко увидеть all=0 рядом с 1 на картах.

Отсюда два практических следствия. Первое: sysctl -w меняет значение только до перезагрузки, а маска net.ipv4.conf.* применяется systemd-sysctl ещё и в момент появления каждого нового интерфейса — VPN-туннель или VLAN, поднятый позже, получит значение из файлов, а не то, что вы вручную выставили на соседней карте. Второе: файлы из /usr/lib/sysctl.d, /run/sysctl.d и /etc/sysctl.d сортируются вместе по имени, и при конфликте побеждает файл с лексикографически последним именем. Одноимённый файл в /etc целиком заменяет вендорский. Поэтому свои настройки я называю с префиксом 99- и после правки проверяю итог, а не содержимое своего файла:

sudo sysctl --system 2>&1 | grep -E '^\* Applying|rp_filter'
systemd-analyze cat-config sysctl.d | grep -n rp_filter
sysctl -a 2>/dev/null | grep '\.rp_filter'

Вторая команда показывает все фрагменты в порядке применения, третья — что в итоге оказалось в ядре на каждой карте.

Не пытайтесь отключить проверку, отредактировав `/usr/lib/sysctl.d/50-default.conf`: обновление пакета вернёт файл. Переопределяйте значения своим файлом в `/etc/sysctl.d/` с именем после `50-default.conf` и помните, что маска `*` в вендорском файле перепишет интерфейсы, для которых вы не задали значение явно.
Цифры и версии: Откуда берётся rp_filter=2 после установки: дефолты дистрибутивов и sysctl.d — схема
Цифры и версии: Откуда берётся rp_filter=2 после установки: дефолты дистрибутивов и sysctl.d. Открыть схему в полном размере

Когда loose mode оправдан и что оставить в эксплуатации

Иногда асимметрия является частью архитектуры: несколько uplink с динамической маршрутизацией, ECMP, туннели, TPROXY или разнесённые входной и выходной маршрутизаторы. Тогда симметризовать каждый поток нельзя или не нужно. В таком проекте я выставляю rp_filter=2 на задействованных интерфейсах и документирую причину. Это не капитуляция перед безопасностью: фильтрацию spoofed-источников разумнее делать там, где известны допустимые префиксы, — ACL на пограничном маршрутизаторе, firewall или правилами провайдера.

Полностью отключаю проверку, то есть использую 0, только когда даже loose lookup противоречит задумке либо источник появляется до установки необходимого маршрута. Ставить all=0 и забывать о параметре на всех картах я не люблю. Для обычных внешних интерфейсов оставляю строгий режим, для осознанно асимметричных — loose. Постоянный файл после исправления нашего стенда был таким:

# /etc/sysctl.d/99-rp-filter.conf
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.enp1s0.rp_filter = 1
net.ipv4.conf.enp2s0.rp_filter = 1
net.ipv4.conf.all.log_martians = 1

Применение и контроль:

sudo sysctl --system
sysctl net.ipv4.conf.all.rp_filter \
  net.ipv4.conf.default.rp_filter \
  net.ipv4.conf.enp1s0.rp_filter \
  net.ipv4.conf.enp2s0.rp_filter

Глобальный all=0 здесь выбран намеренно, чтобы эффективный режим задавался каждой картой. default=1 задаёт режим для интерфейсов, созданных позже, но на Ubuntu и Debian маска net.ipv4.conf.*.rp_filter = 2 из 50-default.conf применяется при появлении каждой карты и перекрывает default; поэтому для VPN, VLAN и контейнерных устройств значение надо задавать явно или проверять после их подъёма.

Если маршрутизация строится по fwmark, помните о net.ipv4.conf.*.src_valid_mark. По умолчанию метка пакета не включается в обратный lookup. Значение 1 нужно, когда одна и та же маркировка действительно участвует в маршрутизации обоих направлений; одной строки sysctl без корректной установки и восстановления mark недостаточно. Я не включаю этот параметр «на всякий случай», потому что ошибочная метка способна создать новую, гораздо менее очевидную асимметрию.

Мой порядок приоритетов короткий. Сначала докажите, какой обратный маршрут рассчитывает ядро. Затем, если асимметрия случайна, исправьте policy routing и сохраните strict mode. Если она предусмотрена проектом — используйте loose mode и перенесите реальную антиспуфинг-проверку на границу сети. На бесконечное перекладывание правил INPUT можно смело забить: пока не проходит проверка источника, до этих правил пакет всё равно не доберётся.

`rp_filter` относится к IPv4. Наличие строгой проверки в `net.ipv4.conf.*` ничего не говорит о фильтрации IPv6-трафика — её нужно проектировать и проверять отдельно.

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

Почему tcpdump видит пакет, если Linux его отбросил?

`tcpdump` получает копию кадра через AF_PACKET раньше, чем IPv4-стек завершает поиск входного маршрута и проверку источника. Захват подтверждает приём интерфейсом, но не доставку в INPUT или приложение.

Почему разрешающее правило nftables не помогает?

При провале `rp_filter` пакет не доходит до цепочки INPUT. При этом правила PREROUTING могут его увидеть, поэтому важно понимать, счётчик какой именно цепочки вы проверяете.

Можно ли просто установить rp_filter=0?

Для короткого диагностического теста — да, если есть безопасный доступ к серверу. Постоянно я предпочитаю исправить policy routing. При намеренной асимметрии обычно достаточно loose mode со значением `2`.

Почему значение 0 на интерфейсе не отключило проверку?

Ядро берёт максимальное значение из `conf/all/rp_filter` и `conf/<интерфейс>/rp_filter`. Если глобально стоит `1`, интерфейсное значение `0` не даст эффекта.

Какое значение rp_filter стоит по умолчанию?

В самом ядре — `0`. Но дистрибутивы переопределяют его: systemd в `50-default.conf` ставит `2` для `default` и всех интерфейсов по маске, Ubuntu дополнительно задаёт `2` в `10-network-security.conf`, RHEL 9 — `1` в `50-redhat.conf`. Смотрите фактические значения через `sysctl`, а не документацию.

Почему значение, выставленное через sysctl -w, пропало?

`sysctl -w` меняет только текущее состояние ядра. После перезагрузки, а для новых интерфейсов и при их появлении, systemd-sysctl снова применит файлы из `/usr/lib/sysctl.d` и `/etc/sysctl.d`. Постоянную настройку кладите в `/etc/sysctl.d/` с именем, которое по алфавиту идёт после `50-default.conf`.

Достаточно ли посмотреть обычный ip route?

Нет. Нужно учитывать `ip rule` и все задействованные таблицы. Входящий пакет моделируйте командой `ip route get <локальный адрес> from <удалённый адрес> iif <карта>` — при провале проверки она вернёт `Invalid cross-device link`, а ответ — командой `ip route get <удалённый адрес> from <локальный адрес>`.

Работает ли rp_filter для IPv6?

Нет, описанный sysctl находится в пространстве `net.ipv4.conf`. Политику проверки источников IPv6 нужно реализовывать и тестировать отдельно.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи