В контейнере `ip netns delete` иногда возвращает успех, но namespace остаётся в `ip netns list` — из-за этого run.sh отдавал код 4 на зелёных тестах (флак 1 из 3). Теперь удаление повторяется до 5 раз с проверкой ns_exists. Refs #21, Refs #56
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 пакет
дошёл до детектора — тест падает.
Запуск
make test-netns # поднять стенд, прогнать тесты, снести стенд
make test-netns-keep # оставить топологию и логи для отладки
make test-netns NETNS_ARGS="--no-tests" # только поднять стенд
make test-netns-ci # то же в привилегированном контейнере (путь для CI)
# в контейнере вход в netns идёт через nsenter (issue #56)
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 в контейнере).
Вход в namespace выполняется через ns_exec(): ip netns exec в контейнере падает
(mount of /sys failed, issue #56), поэтому харнесс сам переключается на
nsenter --net=/var/run/netns/<ns>; режим форсируется AWG_NS_EXEC_MODE.
Состав
| Файл | Назначение |
|---|---|
run.sh |
точка входа: preflight → топология → мок → тесты → снос |
lib.sh |
конфигурация стенда + правила netfilter (nft/iptables) |
topology.sh |
up/down/status/`killswitch on |
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)
test_01— netns и veth-пары созданы, адреса выданы,ip_forward=1.test_02— policy routing:ip rule fwmark 51820→table 51820→ аплинк.test_03— правила меток (OUTPUT/FORWARD) и DROP kill-switch на месте.test_04— мок-endpoint и детектор утечки слушают и залогировалиlisten.test_05— handshake initiation из LAN доходит до мока, мок отвечает (148 → 92 байта), счётчик правила метки растёт.test_06— то же для трафика, инициированного самим шлюзом (OUTPUT-метка).test_07— режимdropмока имитирует недоступность провайдера: ответа нет, пакеты фиксируются; возврат вrespondвосстанавливает стенд.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 |
Pitfalls
ip netns addв контейнере требует--privileged(CAP_SYS_ADMIN) — иначеrun.shзавершится кодом 3 с сообщением «заблокировано окружением».ip netns execиip -n <ns>в контейнере не работают (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>заменены наns_exec <ns> 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.