148 lines
17 KiB
Markdown
148 lines
17 KiB
Markdown
# Стенд стриминга в network namespace
|
||
|
||
В стенде работают **16 отдельных процессов uTun**, каждый со своим veth, адресом, идентичностью и SQLite. Они создают CHAT-канал и вступают в него через штатный JOIN, ждут подписанные профили Merkle и BGP-маршруты, затем используют `stream_publish_open` / `stream_subscribe_open`. Пакеты, подписи, FEC, NCD, reverse и автоматический выбор поставщиков обрабатывает настоящее ядро. Время не ускоряется.
|
||
|
||
Все интерфейсы, маршруты, forwarding, nftables и tc создаются внутри новых user/mount/PID/network namespace. В основной сети выполняется только чтение снимков адресов, маршрутов, правил и qdisc до/после запуска. Нет TUN, default route наружу, именованных `ip netns`, сетевых мостов или firewall-правил на хосте. Внешние инструменты и пакетные зависимости стенд не устанавливает.
|
||
|
||
## Запуск
|
||
|
||
Нужны Linux, Python **3.10+**, iproute2 (`ip`, `tc`), nftables, ethtool, util-linux (`unshare`, `nsenter`), sysctl и поддержка user namespace, veth, bridge, IFB, HTB, netem, TBF и NAT в ядре. Обычно достаточно обычного пользователя с разрешенными user namespace. Если политика системы запрещает их, запуск требует разрешения администратора; стенд сообщает ошибку и не меняет основную сеть.
|
||
|
||
Из корня репозитория:
|
||
|
||
```sh
|
||
make -C tests stream_netns/stream_lab_node
|
||
python3 tests/stream_netns/run.py --network-only
|
||
python3 tests/stream_netns/run.py
|
||
```
|
||
|
||
Проверка детектора на всех 16 настоящих uTun, отдельно от стриминга:
|
||
|
||
```sh
|
||
python3 tests/stream_netns/run.py --nat-only --tcp
|
||
```
|
||
|
||
Резервное определение типа STUN на отдельных портах и переход к измерению ETCP:
|
||
|
||
```sh
|
||
python3 tests/stream_netns/run.py --stun-only --output /tmp/stun-interface-netns
|
||
```
|
||
|
||
Два RFC5780-сервера работают на двух реальных публичных IP, каждый с двумя UDP-портами.
|
||
По очереди проверяются n03 (cone), n14 (restricted) и n05 (dependent mapping).
|
||
Два рабочих порта одного IP/netif используют одну STUN-пробу. Сначала у владельца
|
||
нет ETCP-пиров, затем появляется один наблюдатель, затем независимый помощник.
|
||
Проверяются source=STUN -> source=ETCP, приоритет рабочего измерения и отсутствие
|
||
тестового внешнего порта в рекламе рабочего сокета. JOIN/стриминг в этом режиме не запускаются.
|
||
Случайные тестовые порты получают тот же класс NAT; правила для рабочего 32000 сохраняются.
|
||
Для испытания случайных портов активен один клиент каждого выбранного NAT-шлюза.
|
||
Расширенная классификация restricted/dependent включает реальные таймауты CHANGE,
|
||
поэтому этот режим обычно занимает несколько минут. Результат: `stun-result.json`.
|
||
|
||
`--nat-check` выполняет ту же проверку перед обычными JOIN/Merkle/BGP/стримингом.
|
||
Контроллер сначала запускает только ядро с ручными NAT-раундами, переносит
|
||
подписанные снимки через RPC и создаёт нужные NCD-подключения. Наблюдения,
|
||
испытания входа и TCP-handshake идут по настоящей сети. Помощники измерения
|
||
не требуют готового собственного NAT-профиля. Измерение входа происходит
|
||
до первого исходящего контакта рабочего сокета с помощником.
|
||
|
||
Проверяются отдельно mapping и inbound каждого UDP endpoint, точный собственный
|
||
внешний IP:port, отсутствие ложного STRICT без помощника и корректность TCP listener.
|
||
Ожидания: public — UNCHANGED/OPEN, cone — EIM/OPEN, restricted — EIM/CONTACT_REQUIRED,
|
||
strict — DEPENDENT без универсального внешнего адреса. TCP за NAT без port forwarding
|
||
остаётся UNKNOWN/no_listener. Результаты: `nat-profiles.json`, `nat-result.json`;
|
||
обычная телеметрия также включает `address_profiles`, контакты и причину неполного измерения.
|
||
|
||
Полная baseline-проверка длится 120 секунд после старта доставки, плюс калибровка и JOIN. Она покрывает оборот 16-битного timestamp и несколько циклов проб. Короткая проверка:
|
||
|
||
```sh
|
||
python3 tests/stream_netns/run.py --duration 20
|
||
```
|
||
|
||
Ухудшение WAN, восстановление и отключение подписок:
|
||
|
||
```sh
|
||
python3 tests/stream_netns/run.py --duration 120 --faults
|
||
```
|
||
|
||
Источник за строгим NAT и доступность TCP в дополнение к UDP:
|
||
|
||
```sh
|
||
python3 tests/stream_netns/run.py --publisher n05 --tcp --faults
|
||
```
|
||
|
||
Чтобы требовать именно TCP/STCP, без возможности выбрать UDP:
|
||
|
||
```sh
|
||
python3 tests/stream_netns/run.py --publisher n05 --tcp-only --duration 20
|
||
```
|
||
|
||
Ту же проверку запускает отдельная ручная цель:
|
||
|
||
```sh
|
||
make -C tests check-stream-netns STREAM_NETNS_ARGS='--duration 120 --faults'
|
||
```
|
||
|
||
Стенд не входит в обычный `make check` / `check.sh`. `--rate` задает входной bitrate в битах/секунду **до FEC и протокольных накладных расходов** (по умолчанию 1 Мбит/с). `--output` указывает новый каталог результатов; существующий каталог не перезаписывается. `--seed` фиксирует идентичности и генераторы netem/нагрузки; расписание процессов и случайные ключи канала/эпохи остаются недетерминированными.
|
||
|
||
## Сеть
|
||
|
||
| Сегмент | Узлы с публичными адресами | Узлы за NAT | Подсеть |
|
||
| --- | --- | --- | --- |
|
||
| A | n01, n02 | n03–n06 | 192.0.2.0/24 |
|
||
| B | n07, n08 | n09–n11 | 198.51.100.0/24 |
|
||
| C | n12, n13 | n14–n16 | 203.0.113.0/24 |
|
||
|
||
Всего 25 рабочих namespace: 16 узлов, 3 маршрутизатора стран, 6 NAT-шлюзов, плюс изолированный namespace контроллера. У каждого NAT своя частная LAN с одинаковой `10.64.1.0/24`: совпадение частных адресов между разными NAT намеренное. n16 имеет мобильный тип; его исключение из relay-кандидатов выполняет ядро.
|
||
|
||
| WAN | RTT | Jitter в одну сторону | Loss в одну сторону | Общая полоса в каждом направлении |
|
||
| --- | ---: | ---: | ---: | ---: |
|
||
| A↔B | 50 мс | 2 мс | 0,2% | 50 Мбит/с |
|
||
| A↔C | 100 мс | 5 мс | 0,5% | 30 Мбит/с |
|
||
| B↔C | 150 мс | 10 мс | 1% | 20 Мбит/с |
|
||
|
||
Внутри страны RTT около 2 мс, jitter 0,2 мс, без искусственных потерь. Задержка и loss применяются на **ingress узла через IFB** с классификацией по адресу физического отправителя. Это учитывает каждый переход потока через приложение и работает также для TCP, без искажения от TCP Small Queues на стороне отправителя. WAN-роутеры ограничивают совокупный трафик каждого межстранового линка через TBF; обходных IP-маршрутов нет. GSO/GRO/TSO на veth отключены. Размер очередей конечный; их состояние и drops сохраняются в снимках tc.
|
||
|
||
UDP NAT-профили:
|
||
|
||
- `g_a1`, `g_b1`: одинаковый внешний порт для разных назначений (EIM), вход от любого узла после открытия mapping (EIF).
|
||
- `g_c1`: EIM, вход только от ранее опрошенного IP/порта (APDF).
|
||
- `g_a2`, `g_b2`, `g_c2`: внешний UDP-порт зависит от **IP назначения** (ADM), возврат только по conntrack (APDF). Для известных внешних endpoints порты назначаются явно; это воспроизводимый строгий NAT, а не заявление, что обычный masquerade всегда симметричен.
|
||
|
||
Mapping разрешается после исходящего пакета, время активности/контакта 60 секунд обновляется исходящим трафиком.
|
||
Запрещённый вход отсекается в `prerouting priority raw` до conntrack. Иначе неуспешная
|
||
входящая проба могла закрепить tuple без DNAT и помешать последующему открытию фильтра.
|
||
TCP использует обычный stateful NAT без проброса входящих портов. Калибровка проверяет
|
||
внешний endpoint на разных назначениях, вход до mapping, незапрошенный вход и разрешённый
|
||
вход после исходящего контакта с тем же проверявшим узлом. После калибровки сеть пересоздаётся.
|
||
|
||
После калибровки ее namespace уничтожаются, затем создается новая сеть с теми же параметрами и пустыми NAT mappings/conntrack. Результаты проб не облегчают подключения реальных процессов. Снимки `network-calibration-initial.json` / `network-calibrated.json` относятся к пробам, `network-initial.json` / `network-final.json` — к uTun; команды содержат inode соответствующего namespace. PID в `namespaces.json` записаны относительно PID namespace контроллера.
|
||
|
||
## Что проверяется
|
||
|
||
Для каждого приложения DATA содержит sequence, ID источника, monotonic timestamp, seed и SHA256. Одна запись равна одной входной пачке 6+2; штатная кодограмма около 1300 байт. Получатели проверяют checksum, порядок, явные GAP, итоговый cursor и подписанный END. Начальный пропущенный префикс допустим: подписка начинает с последнего доступного блока. Дальнейшие пропуски должны быть полностью объяснены GAP; поврежденные или повторно доставленные записи приводят к FAIL.
|
||
|
||
One-way latency измеряется по общему CLOCK_MONOTONIC ядра Linux: time namespace не создается. Это внешняя проверка задержки; алгоритм стриминга продолжает использовать свой циклический timestamp. Histogram имеет шаг 1 мс, верхняя корзина 4095 мс; максимальная задержка записывается без ограничения.
|
||
|
||
При `--faults` A↔C снижается до 2 Мбит/с, RTT увеличивается до 300 мс, loss до 5%. После восстановления проверяется возобновление доставки. n02 и n13 затем закрывают подписки; счетчик полученных DATA обязан перестать расти. Перед окончанием проверяется END у оставшихся подписчиков и соответствие числа принятых/пропущенных записей числу записей источника. Каждый снимок проверяет предел данных кольца 32 MiB. GAP в условиях перегрузки записывается в отчет и сам по себе не считается повреждением.
|
||
|
||
Доступный TCP через `--tcp` не гарантирует, что ядро выберет TCP или несколько линков в конкретном запуске. Фактические соединения видны в `utun.log`; выбор поставщиков и пути DATA сохраняются в samples. Этот стенд проверяет доставку через реальные сетевые условия, но не подменяет отдельные unit-тесты криптографии и FEC.
|
||
|
||
## Результаты и диагностика
|
||
|
||
По умолчанию результаты находятся в новом `tests/stream_netns/runs/<date-time>/`:
|
||
|
||
- `result.json` — PASS/FAIL, ошибка, фазы, транспорт, bitrate, наблюдаемые обороты timestamp и результат сравнения основной сети; `host-check.json` — снимок сравнения, с исключением естественно уменьшающихся DHCP lifetime/cache timers.
|
||
- `calibration.json`, `topology.json`, `namespaces.json` — измерения, параметры, PID и inode namespace.
|
||
- `samples.jsonl`, `final-stats.json`, `last-stats.json`, `summary.csv` — доставленные записи, GAP, p50/p95/p99/max latency, буферы, пробы, миграции, source timestamp, rates/lag/stops и пути каждого субпотока. Последний снимок сохраняется также при FAIL.
|
||
- `network.jsonl`, `network-initial.json`, `network-final.json`, `g_*.nft` — точные команды, ошибки, маршруты, tc counters и правила NAT.
|
||
- `nXX/utun.log`, `stderr.log`, `rpc.jsonl`, `node.conf`, `db/` — диагностика и состояние каждого процесса. Конфиги содержат **тестовые** приватные ключи.
|
||
|
||
Категория `stream=debug`, JOIN/Merkle/BGP/connection — `info`; ошибки остальных категорий сохраняются. Для диагностики очередей можно запросить RPC `{"id":"1","op":"stats","loads":"1"}`; обычные снимки не опрашивают отсутствующие peer-load записи, чтобы не засорять лог. Stdout агента зарезервирован для JSONL.
|
||
|
||
Ctrl+C и нормальное завершение закрывают процессы и namespace. PID namespace с `--kill-child` также ограничивает время жизни процессов при аварии launcher; namespace не закрепляются mount-именами на хосте. Файлы результатов сохраняются для анализа.
|
||
|
||
Короткий TCP-only прогон выявил остановку оптимизатора при END первого поставщика, до дочитывания потребителем FEC-пачки. Для этого случая добавлена регрессия в `test_stream_service`: END устанавливает подписанную границу, автоматические пробы и восстановление продолжаются до прохождения consumer cursor за эту границу. Стенд проверяет завершение у всех подписчиков, а не наличие END в одном сетевом handle.
|
||
|
||
Прогон NAT-источника с ухудшением WAN выявил повтор DATA после restart через отстающего поставщика. Restart той же эпохи теперь сохраняет границу уже выданных DATA/GAP: старый подписанный bootstrap может установить общий старт, но не перематывает consumer cursor. Регрессии проверяют повтор bootstrap, перегрузку и два restart до доставки отложенного GAP.
|