Короткий ответ

В stateful filter пакет идёт сверху вниз в цепи и берёт первое совпадение. Нижнее allow не лечит верхний drop; нижний drop не закрывает верхний any-any. Shadowing — когда широкое правило выше узкого и всегда срабатывает первым. Лечение: выгрузить цепь, пометить пересечения, переставить, удалить мёртвое. Не flush, не policy accept, не дублировать allow «на всякий» в конце.

На pfSense/OPNsense та же идея first-match на интерфейсе; floating с порядком выше интерфейсных. Не выдумывайте отдельный CLI вендора: читайте номер правила и states.

WAN 203.0.113.10, LAN 10.0.10.0/24, DMZ 10.0.30.0/24, mgmt 10.0.99.0/24.

Симптомы и как отличить

Админ добавляет accept LAN→DMZ:443 в конец, сервис мёртв. В середине цепи уже есть drop ip saddr 10.0.10.0/24 oifname dmz0. Или наоборот: в начале leftover accept ip protocol tcp, а «жёсткий» drop внизу никогда не работает — лишние порты.

ПризнакСмысл
Allow counter = 0, drop выше растётshadowing allow
Drop counter = 0, allow выше широкийshadowing drop, дыра
Два одинаковых allowмёртвый дубль
Работает после disable правила №3№3 было первым матчем

Отличие от «сервис не слушает»: на хосте ss LISTEN, пакет виден на in-iface tcpdump до фильтра и не виден на out после. Отличие от блокирует сервис: там ищем какой drop; здесь — почему правильный allow не бьётся.

Возможные причины

  1. Копипаст «временного» drop VLAN выше рабочих allow.
  2. Any-any или accept in-interface=LAN в начале — any-any в проде.
  3. Пересечение address-list: хост в двух листах, первое правило шире.
  4. IPv4-правило vs клиент IPv6 (разные цепи, «противоречие» ложное).
  5. raw/prerouting drop до filter (не видно в chain forward).
  6. На RouterOS jump в custom chain, return, затем drop — человек смотрит не ту цепь.
  7. FastTrack: пакет не доходит до нижних правил с action=log.

Диагностика

1. Снимок цепи

sudo nft list ruleset -a > /root/fw-shadow-$(date +%F).nft
/export file=fw-shadow
/ip firewall filter print without-paging
/ip firewall raw print
/ip firewall nat print

Разложите одну цепь (forward или input). Не мешайте NAT и filter в одной «простыне без номеров».

2. Выберите один 5-tuple заявки

Пример: 10.0.10.50 → 10.0.30.10 tcp/443. Пройдите глазами каждое правило: матчится? Если да — это победитель, всё ниже для этого пакета мертво.

Запишите в таблицу:

№ / handlematch 5-tuple?actioncounter
establishedда, если stateacceptбольшой
drop LAN→DMZда для newdropрастёт при клике
allow :443да, но не достижимоaccept0

3. Трассировка без отключения фильтра

nft:

sudo nft add rule inet filter forward position 0 \
  ip saddr 10.0.10.50 ip daddr 10.0.30.10 tcp dport 443 \
  counter log prefix "SHDW " flags all

Один пакет — одна серия log с hook. Удалите handle после съёма.

RouterOS: /tool torch или временный action=log вверху, затем сравнение с stats нижних. Не action=accept на всё.

4. Address-list и множества

/ip firewall address-list print where address=10.0.10.50
sudo nft list set inet filter mgmt_net

Хост в mgmt и в guests одновременно — первое правило с этим множеством решает судьбу.

На концептуальном pf: pfctl -s rules с номерами, pfctl -v -s rules — evaluations. Первое matching pass/block.

Решение

Сценарий A. Узкий allow ниже широкого drop

Переставьте allow выше drop, сохраняя established первым:

Порядок forward:

  1. ct/connection established,related accept
  2. allow LAN 10.0.10.0/2410.0.30.10 tcp 443
  3. drop LAN → DMZ (остальное)
  4. drop invalid / drop all

nft: правьте файл, nft -c -f, reload. Не набор nft insert вслепую без выгрузки.

RouterOS: move номера allow перед drop. Комментарий на обоих.

Сценарий B. Широкий allow выше «запрета админки»

Если в input первым стоит accept tcp dport 80,443 с WAN — GUI торчит, нижний drop www бесполезен. Сужайте верхнее: не WAN, а LAN/mgmt, либо уберите www с 0.0.0.0. Это чинится как админка, но корень — порядок.

Сценарий C. Дубли

Два одинаковых accept — удалите нижний после сверки counters (должен быть 0). Документируйте в ревизии.

Сценарий D. Raw/prerouting

sudo nft list table inet raw
/ip firewall raw print stats

drop в raw notrack обходит ожидания по filter. Перенесите политику в filter либо явно опишите raw в матрице.

Сценарий E. Jump-цепи

Нарисуйте: forward → jump LAN-in → return → drop. Allow должен жить в той цепи, куда пакет jump'ает. Allow в корне ниже jump+drop не спасёт, если jump не вернулся.

Откат: nft -f снимка, /import export, backup RouterOS.

Как проверить, что проблема устранена

  1. Для контрольного 5-tuple растёт один осмысленный allow, не drop выше.
  2. Старый широкий drop всё ещё ловит нежелательное (проверьте отрицательный тест: порт не из матрицы с LAN в DMZ должен падать).
  3. Скан своего WAN 203.0.113.10 не стал шире.
  4. В выгрузке нет двух правил с одинаковым match и разным action.
sudo nft list ruleset -a

Передайте ревьюеру таблицу handle/match/action/counter по заявке.

Если не помогло

  • Пакет в другой цепи (input vs forward, Docker/bridge).
  • NAT меняет dst до filter — смотрите conntrack dst после DNAT.
  • IPS/GeoIP вне этой таблицы.
  • Клиент идёт на другое имя/VIP.
  • Правило на tcp, трафик udp (QUIC).

Профилактика

  • Стиль: established → allow узкие → drop групповые → drop all.
  • Запрет двух правил на один 5-tuple без комментария «shadow intentional» (таких почти не бывает).
  • Ревизия раз в квартал: нулевые allow старше 90 дней — кандидат на удаление.
  • Лог критичных drop помогает ловить регресс порядка.
  • Имена interface-list, не голые ether, после перепатчевания портов.

FAQ

nft не first-match?

Обычные filter-цепи с accept/drop — да, первое совпадение (если нет continue/вердикта, который пропускает дальше). Не пишите набор accept ниже drop того же матча.

Нужно ли «последнее правило wins» как в ACL Cisco interface в другую сторону?

На IOS некоторые ACL разные. На nft/RouterOS filter/pf — first-match. Не переносите привычку «нижнее перекрывает» без проверки.

Можно ли оставить shadowed allow «для документации»?

Нет. Документация — матрица и комментарий. Мёртвое правило врёт на аудите.

insert vs add в RouterOS

add без place-before — в конец. Именно так allow оказывается под drop. Всегда указывайте место.

Почему после move «починилось», но ночью снова сломалось?

Автоскрипт/Ansible заливает старый порядок. Чините источник, не только live.