
Детерминированная сетевая песочница для тестирования правил nftables. Использует эфемерные сетевые пространства имён Linux (netns) и Scapy для безопасной проверки логики межсетевого экрана.
Зачем NSE? • Контракт оракула • Проверки на уровне сети • Возможности • Требования • Установка • Быстрый старт • Как это работает • Структура проекта
Тестирование наборов правил межсетевого экрана на работающей системе Linux сопряжено со значительными рисками: некорректные правила могут разорвать сеансы управления по SSH, допустить утечку трафика в открытом виде во время тестирования или оставить на хосте активные «осиротевшие» таблицы межсетевого экрана.
Network Sandbox Engine (NSE) предоставляет безопасный и воспроизводимый стенд для тестирования. Он создаёт эфемерные сетевые пространства имён Linux, соединяет виртуальные пары ethernet, компилирует наборы правил nftables и внедряет синтетические пакеты уровня 2 и уровня 3 с помощью Scapy. Вся обработка происходит внутри пространства имён песочницы: состояние межсетевого экрана хоста никогда не изменяется.
Ключевые архитектурные свойства:
nse_<uuid>) и полностью удаляются при завершении работы.nse/ — это около 1200 операторов при 98% покрытии тестами.NSE создаёт сетевые пространства имён, загружает наборы правил nftables и читает события трассировки ядра, поэтому работает от имени root. Он не открывает сокет, порт или RPC-эндпоинт какого-либо рода — это библиотека и CLI, которые вы вызываете, и он обладает привилегиями только на время выполнения.
Версия 2.1.0 удалила веб-интерфейс на FastAPI/Svelte, который поставлялся в более ранних выпусках. Этот интерфейс работал в процессе от имени root начиная с версии 2.0.0, что представляло собой большую поверхность атаки для инструмента тестирования; код остаётся в истории git под тегом v2.0.0, если он вам нужен.
Тест межсетевого экрана — это негативное утверждение — «этот пакет не прошёл» — а негативное утверждение ничего не стоит, если не известно, что инструмент работает. Монитор трассировки, который так и не подключился к ядру, и межсетевой экран, который заблокировал всё, дают побайтово идентичный вывод.
Поэтому NSE отказывается сообщать вердикт, который он не может показать как измеренный:
| Гарантия | Механизм |
|---|---|
| Монитор был подключён до первого тестового пакета | Внедряется канарейка готовности и повторно внедряется до тех пор, пока не будет зафиксирован её трассировочный след в ядре. Нет наблюдения — нет запуска. |
| Монитор был всё ещё подключён после последнего | После внедрения запускается канарейка живости. Если она пропущена, поток вердиктов объявляется усечённым. |
| Парсер понял то, что сообщило ядро | Строки трассировки, не совпавшие ни с одним шаблоном, подсчитываются, и любое значение больше нуля является ошибкой, а не отладочным логом. |
| Монитор не завершился молча | Цикл чтения фиксирует причину своего завершения — чистая остановка, неожиданный EOF, тайм-аут или сбой — и приемлема только чистая остановка. |
| Отсутствующий вердикт — не успех | CLI-раннер завершается с ошибкой, когда число наблюдаемых вердиктов отличается от ожидаемого, в любую сторону. |
Канареечные пакеты исключаются из результатов по trace id, поэтому они никогда не появляются в вашем потоке вердиктов.
Набор тестов доказывает это, а не утверждает: make test-blind заставляет
парсер не понимать ничего, и сборка завершается с ошибкой, если раннер не завершается
с ненулевым кодом. Эта задача выполняется в CI при каждом push.
nft monitor trace сообщает о том, что решил набор правил. Он не может сообщить о том, что
покинуло интерфейс, на котором набор правил никогда не сопоставлялся, — а это именно тот вопрос, который на самом деле задаёт утверждение об отсутствии утечек. PCAPAsserter покрывает это: обёртка над AsyncSniffer, которая захватывает трафик на указанном интерфейсе и возвращает кадры, чтобы тест
мог утверждать, что стимул не породил ни одного.
from nse import PCAPAsserter
asserter = PCAPAsserter(iface="veth-host")
await asserter.start()
# ... send the stimulus ...
leaked = await asserter.stop()
assert not leaked, leaked
Действует то же правило, что и для trace oracle, и его должен обеспечивать вызывающий код: сниффер, который так и не подключился, и межсетевой экран, заблокировавший всё, оба возвращают пустой список. Захватите стимул один раз при очищенном наборе правил и требуйте, чтобы кадр был увиден, прежде чем утверждать его отсутствие.
Фильтр по умолчанию подавляет шум link-local и ничего больше.
DEFAULT_FILTER исключает ARP и типы ICMPv6 133-137 — Neighbour Discovery, которые несут hop limit 255 и не могут достичь удалённого наблюдателя. Всё остальное захватывается, включая ICMPv6 echo на глобально маршрутизируемый адрес. Аргумент filter объединяется с этим значением по умолчанию; replace_default_filter=True использует только ваш. Этот обходной путь существует потому, что фильтр компилируется в BPF и вычисляется в ядре: пакет, который он исключает, никогда не достигает пространства пользователя, поэтому ни один потребитель не может восстановить его ниже по потоку. До версии 2.1.2 значение по умолчанию читалось как not arp and not icmp6, что отбрасывало и маршрутизируемый ICMPv6 — см.
issue #14.