# Стенд стриминга в 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 ``` `--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//`: - `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.