94d3a28adf
Приведение документации к фактическому поведению после ревью PR #53 (раунд 2): точка входа для CI без make (контракт 0/1/3/4), приоритет AWG_NS_EXEC_MODE над make-алиасом, сборка без сети (DEV_SKIP_BUILD=1). Refs #21 Refs #56
134 lines
10 KiB
Markdown
134 lines
10 KiB
Markdown
# 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/<ns>`; режим форсируется `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` | `<repo>/artifacts` | каталог для `netns-report.json` (`ci.sh`) |
|
||
|
||
## 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`.
|