# tests/netns — сетевой тестовый стенд Задача **#21**, используется тестами split-tunnel (#40, #41, #43). Цель: проверять маршрутизацию, policy routing, метки трафика и fail-closed **без реального VPN-провайдера** и без доступа к продакшн-сети. ## Топология ``` client ns gw ns (шлюз awg-dash) uplink ns («ISP») 10.213.0.2/24 ──veth──► 10.213.0.1/24 10.214.0.1/24 ──veth──► 10.214.0.2/24 (LAN-устройство) ip_forward=1, ip rule fwmark 51820 мок-endpoint udp/51820 → table 51820 → default via uplink детектор утечки 198.51.100.9:9999 ``` | Namespace | По умолчанию | Роль | Адреса | |---|---|---|---| | `awgt21-client` | `AWG_NS_CLIENT` | LAN-устройство за шлюзом | `10.213.0.2/24` | | `awgt21-gw` | `AWG_NS_GW` | хост со шлюзом: forwarding, policy routing, netfilter | `10.213.0.1/24`, `10.214.0.1/24` | | `awgt21-uplink` | `AWG_NS_UPLINK` | «ISP»: мок WireGuard-endpoint + детектор утечки | `10.214.0.2/24`, `198.51.100.9/32` | Правила в `gw`: `ip rule fwmark 51820 lookup 51820` + `table 51820 default via 10.214.0.2` (как в `wg-quick`), метки ставит netfilter на исходящий трафик шлюза (OUTPUT) и на транзитный трафик LAN (FORWARD). Kill-switch — правило DROP в FORWARD для трафика LAN, уходящего напрямую в «публичную» сеть `198.51.100.0/24` (RFC 5737 TEST-NET-2). Адрес детектора утечки поднят на `lo` аплинка: если при включённом kill-switch пакет дошёл до детектора — тест падает. ## Запуск ```bash make test-netns # поднять стенд, прогнать тесты, снести стенд make test-netns-keep # оставить топологию и логи для отладки make test-netns NETNS_ARGS="--no-tests" # только поднять стенд make test-netns-ci # то же в привилегированном контейнере (путь для CI) bash tests/netns/ci.sh # точка входа для CI #17: контейнер без make # (make отдаёт 2 на любом провале — см. ниже) bash tests/netns/topology.sh status # состояние топологии bash tests/netns/topology.sh down # снести топологию вручную bash tests/netns/topology.sh killswitch on # включить fail-closed правило ``` Требования: Linux, **root** (или CAP_NET_ADMIN + CAP_SYS_ADMIN), `ip`, `python3`; backend netfilter — `nft` или `iptables` (в alpine-образе приложения есть только `iptables`, на Debian/Ubuntu — оба). Коды возврата: `0` — тесты прошли, `1` — тесты упали, `3` — «заблокировано окружением» (нет прав на создание netns), `4` — ошибка самого стенда (сюда же нормализуются неожиданные коды утилит, например `255` от `ip netns exec` в контейнере). **В CI (#17) используйте `bash tests/netns/ci.sh`, а не `make test-netns-ci`:** GNU make возвращает `2` на любой упавший рецепт, поэтому через make контракт `0/1/3/4` не виден и пайплайн не отличит «раннеру не дали прав» (`3`) от «регрессия стенда» (`4`). `ci.sh` запускает тот же контейнер напрямую и отдаёт его код как есть (`2` у него означает «сломан вызов/нет docker/нет образа»). Вход в namespace выполняется через `ns_exec()`: `ip netns exec` в контейнере падает (`mount of /sys failed`, issue #56), поэтому харнесс сам переключается на `nsenter --net=/var/run/netns/`; режим форсируется `AWG_NS_EXEC_MODE` (`auto|ip|nsenter`). Тот же режим через make — `NETNS_EXEC_MODE` (алиас; `AWG_NS_EXEC_MODE` имеет приоритет), например: `AWG_NS_EXEC_MODE=ip make test-netns-ci`. ## Состав | Файл | Назначение | |---|---| | `run.sh` | точка входа: preflight → топология → мок → тесты → снос | | `ci.sh` | точка входа для CI #17: тот же прогон в контейнере без make, коды `0/1/3/4` доходят до пайплайна | | `lib.sh` | конфигурация стенда + правила netfilter (nft/iptables) | | `topology.sh` | `up`/`down`/`status`/`killswitch on|off` | | `mock_wg_endpoint.py` | мок WireGuard-endpoint: отвечает на handshake initiation | | `leak_sink.py` | детектор утечки: UDP+TCP `198.51.100.9:9999` | | `probe.py` | генератор тестового трафика из указанного netns | | `netns_common.py` | утилиты для тестов (ns_exec, счётчики правил, JSONL-логи) | | `run_tests.py` | прогон unittest + JSON-отчёт | | `test_netns_stand.py` | 8 интеграционных тестов стенда | ## Что проверяют тесты (`test_netns_stand.py`) 1. `test_01` — netns и veth-пары созданы, адреса выданы, `ip_forward=1`. 2. `test_02` — policy routing: `ip rule fwmark 51820` → `table 51820` → аплинк. 3. `test_03` — правила меток (OUTPUT/FORWARD) и DROP kill-switch на месте. 4. `test_04` — мок-endpoint и детектор утечки слушают и залогировали `listen`. 5. `test_05` — handshake initiation из LAN доходит до мока, мок отвечает (148 → 92 байта), счётчик правила метки растёт. 6. `test_06` — то же для трафика, инициированного самим шлюзом (OUTPUT-метка). 7. `test_07` — режим `drop` мока имитирует недоступность провайдера: ответа нет, пакеты фиксируются; возврат в `respond` восстанавливает стенд. 8. `test_08` — kill-switch: при выключенном правиле трафик доходит до детектора (положительный контроль), при включённом — утечки нет. ## Параметры (переменные окружения) | Переменная | По умолчанию | Назначение | |---|---|---| | `AWG_NETNS_PREFIX` | `awgt21` | префикс имён namespace/veth | | `AWG_NS_CLIENT` / `AWG_NS_GW` / `AWG_NS_UPLINK` | `awgt21-*` | имена namespace | | `AWG_MOCK_PORT` | `51820` | порт мока WireGuard-endpoint | | `AWG_LEAK_IP` / `AWG_LEAK_PORT` | `198.51.100.9` / `9999` | адрес и порт детектора утечки | | `AWG_FWMARK` / `AWG_ROUTE_TABLE` | `51820` / `51820` | метка и таблица policy routing | | `AWG_FW_BACKEND` | `auto` | `nft`, `iptables` или автоопределение | | `AWG_NETNS_WORKDIR` | `mktemp -d` | каталог логов и отчёта стенда | | `AWG_NS_EXEC_MODE` | `auto` | вход в netns: `ip`, `nsenter` или `auto` (проба) — issue #56; make-алиас — `NETNS_EXEC_MODE` | | `AWG_NS_PRIVILEGED` | `1` | только для `ci.sh`: `0` — прогон без `privileged` (проверка ветки «код 3») | | `AWG_DASH_TEST_ARTIFACTS` | `/artifacts` | каталог для `netns-report.json` (`ci.sh`) | ## Pitfalls * **`ip netns add` в контейнере** требует `--privileged` (CAP_SYS_ADMIN) — иначе `run.sh` завершится кодом 3 с сообщением «заблокировано окружением». * **`ip netns exec` и `ip -n ` в контейнере не работают** (iproute2 6.x перемонтирует `/sys`: `mount of /sys failed: Operation not permitted`, rc=255), хотя `ip netns add` и `nsenter --net=...` работают. Поэтому в скриптах только `ns_exec`/`ns_prefix` (`lib.sh`) и `ns_exec_mode()` (`netns_common.py`), а все `ip -n ` заменены на `ns_exec ip ...` (issue #56). Форсировать режим: `AWG_NS_EXEC_MODE=nsenter|ip`. * **rp_filter** в шлюзе выставлен в `2` (loose): маркированная политика даёт асимметрию reverse-пути, strict-режим молча дропает ответы. * **Мок не умеет криптографию**: он повторяет раскладку сообщений WireGuard (тип и длины), но не выполняет Noise IK handshake. Годится для топологии, меток, policy routing и fail-closed — **не** для проверки шифрования, MTU и пропускной способности. * **`nft` в alpine-образе нет**: в контейнере используется `iptables` (nft-backend). Стенд поддерживает оба backend; при отладке форсируйте `AWG_FW_BACKEND=iptables`. * **`ping` в образе нет** (пакет `iputils` не установлен) — тесты не используют ICMP: связность проверяется UDP-трафиком через мок и детектор утечки. * **Остатки после падения**: `bash tests/netns/topology.sh down` и `ip netns list`.