Files
utun2/doc/tasks.md
T
evgeny a96d5926c7 Fix Android external network monitoring and startup readiness
Select frontend network callbacks for auto_sockets=android instead of opening a denied netlink monitor. Preserve monitor descriptor ownership, keep native monitoring failures fatal, and release caller config on failed Android core creation and restart.

Register ConnectivityManager before requesting startup, report readiness after SERVICE_STARTED, and refresh addresses after initialization with serialized updates and generation checks. Add regression coverage for denied netlink, socket failures, address updates, and fd=0 preservation. Record the separate cancellation issue for startup before readiness.

Validation: full check.sh passed, including proxy/burst/load integrations. Clean Android build installed successfully on SM-A525F and Q8; logcat confirms both cores initialized and entered the poll loop with external monitoring and post-start address refresh.
2026-10-12 16:01:26 +02:00

36 KiB
Raw Blame History

Задачи по проекту

Сюда пишется список задач с короткой аннотацией. Если задача большая и имеет ТЗ - то ТЗ оформляется отдельным файлом, а сюда помещается аннотация и ссылка на ТЗ.

Текущие задачи

[ ] Android: отмена асинхронного запуска до готовности ядра — при проверке мониторинга 12.10.2026 обнаружено: NativeLib.stop() пропускает native stop, если isReady()==false, а instance_lite_stop() возвращается при g_inst==NULL. Поэтому запрос stop до завершения конструктора может не отменить запускаемый поток. Согласовать отмену с состоянием потока и единым сигналом завершения; проверить stop до SERVICE_STARTED, отказ конструктора и последующий start.

[ ] Android: повторные отказы persisted node snapshots — замечено 12.10.2026 на устройстве D2AR10P3600820 при проверке live media. В сохранённой БД ряд узлов регулярно получает invalid persisted snapshot в topo_node_sqlite_snapshot_load, затем NCD сообщает node not found; повторные попытки засоряют лог. Лог: /tmp/stream-av-device-initial.log. Причина пока не установлена: текущий текст объединяет отказ deserialize, несовпадение ID и отказ verify. Разделить диагностику этих ветвей и проверить повторные попытки загрузки отвергнутого снимка. Существующую БД устройства при проверке стриминга не очищали.

[+] STUN IPv4: резервное определение типа NAT по IP/netif — уточнённый план 12.10.2026 в doc/nat_detection.txt. Одна группа и отдельный тестовый UDP-порт на уникальный IP/netif, общий кэш с generation/TTL. При NO_OBSERVER/NO_HELPER применять пригодную оценку STUN с явным source; измеренный ETCP приоритетен. mapped адрес тестового сокета не переносить на рабочий. Проверить дубликаты, переход STUN -> ETCP, различия по портам и жизненный цикл; namespace-стенд. Реализовано 12.10.2026: группировка, кэш/TTL, история портов через reload, source в подписанном NODEINFO/SQLite/мониторе и приоритет частичного ETCP. 145/145 тестов; три ASAN-теста без утечек; реальные cone/restricted/dependent NAT и переключение STUN -> ETCP прошли в изолированном стенде.

[ ] STUN IPv6 — отдельный этап: IPv6 endpoint/config/DNS и XOR-MAPPED, scope/netif, тесты IPv6/NAT66. IPv4-оценки не переносить на IPv6 или TCP.

[ ] check.sh: изоляция сетевых integration-тестов — после основного набора автоматически запускает check-proxy/burst/load при доступном sudo. Скрипты меняют rp_filter, таблицу 100 и iptables главного namespace; cleanup выставляет rp_filter=1 вместо сохранённого значения. Перенести тесты в отдельный namespace и проверять неизменность родительской сети, как в tests/stream_netns.

[ ] Стриминг: реализация по stream.txt — 10.10.2026: ТЗ и этапы: doc/stream.txt, разделы 15–16. Основа данных реализована в src/stream: подписанные FEC-блоки 1300 байт, общий буфер 32 МБ, относительные timestamp-метрики, источник с таймером 100 мс и категория stream. test_stream_core прошёл; ./check.sh: 143 passed, 0 failed, 0 skipped; proxy/full, burst и load — PASS. 11.10.2026 подключены жизненный цикл CHAT, локальные handles/API и события потребителя, атомарные опции мембера через Merkle, подписанные профили и BGP-фильтр кандидатов. test_stream_service прошёл на четырёх ядрах: две группы, смена эпохи/членства, restart, rollback профиля, закрытие из callback и нулевой остаток аллокаций после destroy. Лог: /tmp/stream-service-test-final.log. Исправлена выявленная утечка BGP node_paths при удалении узла. Общий ./check.sh: 144 passed, 0 failed, 0 skipped; proxy/full, burst и load — PASS. Лог: /tmp/stream-service-check.log (107 с). 11.10.2026 подключена сетевая выдача raw ETCP через собственный NCD owner: INFO/поколения, handles/watermark, общий старт от первого DATA, курсоры/пути/GAP/LOOP, подпись END и аренда/RENEW. Routed REVERSE_REQ/CLOSED подписываются end-to-end. Явные receive API позволяют открыть/закрыть выдачу и начать новую попытку. Сетевая цепочка источник -> relay -> получатель, две группы/FEC на одном ETCP, GAP/restart, повторы/reverse, END, lease timeout и REINIT прошли под ASAN/LeakSanitizer без ошибок/утечек. Логи: /tmp/stream-network-asan-core.log и /tmp/stream-network-asan-service.log. После восстановления обычной сборки ./check.sh: 144 passed, 0 failed, 0 skipped; proxy/full (9/9), burst и load — PASS. Лог: /tmp/stream-network-check.log (88 с). Wire и численные пределы зафиксированы в doc/stream.txt, раздел 17. 11.10.2026 подключены timestamp-метрики/rates/stops, nonce/метрики INFO и RTT uint32 мс, A/D/X/Q выдач/пиров/модуля, интервалы 200мс и история 10с, подтверждение перегрузки 500мс. OVERLOAD с frozen rates, FEC/альтернативами при выборе, SELECT/CLOSED, неизменным сроком 2с и fallback; глобальный бюджет нескольких пиров требует backlog и локальной загрузки, CPU отдельно не режет. 4-секундный предел возраста control/ожидающих заданий, GAP/X при истечении и наблюдаемый статус accepting. Частичная/сильная/глобальная разгрузка, короткий глитч и поздний SELECT прошли под ASAN/LeakSanitizer без ошибок/утечек. Логи: /tmp/stream-pressure-asan-core.log и /tmp/stream-pressure-asan-service.log. Обычная сборка восстановлена; ./check.sh: 144 passed, 0 failed, 0 skipped; proxy/full (9/9), burst и load — PASS. Лог: /tmp/stream-pressure-check.log (90 с). Wire версии 2 и пределы обновлены в doc/stream.txt, раздел 17. 11.10.2026 подключён автоматический планировщик: stream_subscribe_open начинает получение, ручной режим выделен в stream_subscribe_manual_open. INFO-only пробы, цикл 32/1с/30с, проверенные пути/RTT/метрики/слоты, backoff, распределение FEC, перенос с перекрытием и failover. Первый DATA задаёт общий N; пустой источник и курсор last+1 ждут данные, END без DATA также подписан и доставляется. Сохранён желаемый режим frontend; stream owners удерживают ACTIVE до close, затем восстанавливается STANDBY. Статус/настройки — stream_selection_get_info/configure; wire версии 3 и пределы — doc/stream.txt, раздел 17. На четырёх ядрах проверены 400 непрерывных пачек, общий планировщик владельцев, цикл 30с, FEC spread, overlap до проверенного DATA, остановка relay/CLOSED и восстановление без разрыва ETCP. Core/service, NCD и standby transport прошли ASAN/LeakSanitizer; остаток аллокаций — ноль. Логи: /tmp/stream-optimizer-asan-{core,service,ncd,standby}.log. Односторонняя смена stream generation восстанавливается по nonce INFO без разрыва ETCP и вращения эпох другой стороны; регрессия и весь service-тест прошли ASAN/LeakSanitizer: /tmp/stream-generation-asan-service.log. ./check.sh: 144 passed, 0 failed, 0 skipped; proxy/full (9/9), burst и load — PASS. Лог: /tmp/stream-optimizer-check.log (97с). Следующий этап — длительные/масштабные сетевые проверки: NAT/reverse, мультилинк, одновременная перестройка, несколько автоматических источников/групп и mobile standby на устройстве.

[+] test_reality_tls: коллизии порта слушателя — 10.10.2026: В общем прогоне 140 passed, 1 failed: stcp_server_create не смог занять порт 34922, bind errno=98 (EADDRINUSE). Причина: выбор порта как 34000 + time(NULL) % 1000 без гарантии его доступности. Слушатель создаётся с port=0; stcp_server_get_port возвращает назначенный ОС порт для подключения клиента. Логи показывают фактические порты REALITY и HTTPS. Сборка прошла; 8 параллельных TLS/HTTP-тестов прошли при занятом всём прежнем диапазоне 34000–34999. ./check.sh: 141 passed, 0 failed, 0 skipped; proxy/full, burst и load — PASS.

[ ] Member files: исчерпание кеша результатов массовых изменений — 10.10.2026: Воспроизведено test_member_files_browser::recursiveDeletion(server-busy): из 76 объектов удаляются 64, следующая команда получает MF_BUSY, остаются 12. storage.c: STORE_RECEIPTS=64, RECEIPT_TB=1200000 (120 секунд). Лимит общий для успешных COMMIT/MKDIR/RENAME/REMOVE; затрагивает также рекурсивное копирование. Нужен отдельный этап управления временем жизни receipts с сохранением идемпотентности повторных запросов; простое вытеснение старых записей нарушает гарантию повторного ответа. После исправления изменить регрессию на успешное удаление более 64 объектов и проверить повторные запросы/потерю ответа.

[ ] Desktop: провалы вероятности VAD при компрессии — диагностика 09.10.2026: Воспроизведение пользователя: длительная гласная, затем временная нечувствительность; ближе к концу лога несколько повторов. Новый лог 14:32:14–14:34:38: на 14:34:07–18 и 14:34:20–38 все 556 окон с RMS > -30dBFS имеют p < 0.5; PCM непрерывен, errors=0, RX-gate не мешает, clips=5 не растёт. Сигнал не исчезает, порог остаётся 0.5. --probe-stream: C-обёртка + реальный AGC + две Silero на одинаковом PCM, отличие — один reset состояния второй модели. Синтезированный flite ah растянут до 24.576с: без AGC/клиппинга p первой секунды следующей речи 0.027 против 0.674 после reset. При AGC 1dB (clips=0) — 0.012 против 0.662. Влияние истории модели подтверждено, провал на десятки секунд целиком не воспроизведён. Лог стенда: /tmp/vad-vowel-context.log; запуск: test_silero_vad lib/silero_vad.onnx --probe-stream /tmp/vad-vowel-context48.pcm 993. Вход стенда: mono s16le/48кГц, reset_window — индекс окна 512/16кГц; режим проверяет без AGC, 1dB и 15dB. Автоматические сбросы в рабочем VAD не добавлялись; test_silero_vad PASS. Уточнение: низкая вероятность держится десятки секунд, затем распознавание восстанавливается; краткий клиппинг это не объясняет. Локальный лог 14:00:10–14:00:58: 15 сбросов AEC при пропусках референса, 16 clipped samples в VAD; vad DEBUG не был включён. Добавлены секундные уровни raw/clean/reference и причина bypass/reset AEC; отдельное предупреждение о клиппинге VAD. Секундный отчёт захвата работает также без PCM: возраст кадра, очередь, максимальная задержка и предупреждение о простое. TRACE каждого окна содержит DC, число нулей и gain вместе с вероятностью/уровнем/паузой между окнами; видны сбросы и RX-gate. В локальном build/vibechat.cfg включены vad=trace, aec/radio/call=debug, general=info; после диагностики вернуть обычные уровни. Стенд --probe повторяет одинаковый PCM без reset: AGC 15/30dB после паузы обрезает пики (468/5625 samples на чистой записи). Полного провала всей фразы почти до нуля стенд не воспроизвёл; нужен повтор живой речи с новыми логами для связи с AEC/AGC. Алгоритмы обработки сигнала пока не менялись; исправление причины выбирается по результатам повтора.

[ ] Linux headless CMake: актуализировать зависимости общего ядра — 07.10.2026: чистая сборка tools/chatgui-android/build_headless останавливается на lib/speex_aec.c: speex/speex_echo.h не найден. utun_setup_sources включает этот исходник, но UTUN_INCLUDE_DIRS не содержит lib/speexdsp/include. Проверить полный состав Speex/voice зависимостей отдельного каркаса; основная Autotools C-сборка и Android NDK-сборка проходят.

[ ] REALITY: исправить формат ClientHello и продолжение TLS-потока — 07.10.2026: supported_groups и signature_algorithms не содержат внутренние двухбайтовые длины списков. После ClientHello/ServerHello идёт STCP без TLS record framing; это распознаётся пассивным наблюдателем.

[ ] Завершить проверку реакций и голосов на SM — 07.10.2026: причина отказа установлена: у записей корней signed_by=0/NULL были нулевые/отсутствующие подписи дерева. Проверка корня ключом канала остаётся обязательной; добавлены причины отказа и проверка доверенного Ed25519. Исправлено применение подтверждённой подписи при равной/старой версии профиля и её сохранение при нулевом блоке в более свежем профиле. Создание канала проверяет обе подписи корня до записи собственного мембера. На local восстановлены шесть подписей канальными приватными ключами без изменения остальных полей записей. Разовый скрипт и резервная копия: /home/user/utun2-backups/root-certificates-20261007. Проверены все 18 участников local и 17 участников копии SM; тесты требуют отказа при нулевой/поддельной подписи. Linux/Android/desktop собраны; ./check.sh: 133 passed, 0 failed, 1 skipped; proxy/full, burst, load и 7 Qt-тестов — PASS. Осталось установить APK на SM (RZ8R72MG64B) и проверить реакцию/голос на устройстве: SM отключился от ADB во время работы.

[ ] rlottie: разбор предупреждений GCC 15 в импортированных исходниках — 05.10.2026: при сборке tools/chatgui/rlottie_src GCC сообщает -Wuninitialized в VArenaAlloc::reset(): fFirstBlock/fFirstSize/fFirstHeapAllocationSize читаются после явного вызова деструктора. Проверить сохранение значений до разрушения и повторный reset с реальными выделениями. Также проверить -Wfree-nonheap-object в vcow_ptr: статическая модель default_s сохраняет базовую ссылку. Лог: /tmp/utun-rlottie-vendor-build.log. Сборка библиотеки и подключение её экспортированного CMake-пакета к ChatGUI на Linux прошли; Windows ещё не проверен.

[ ] ChatGUI Windows: подтверждение обновления runtime компилятора — 05.10.2026: cc-probe.log: cc1 запускается из корня, но в tools/chatgui/build завершается с 0xC0000139; локальные DLL GCC/math/runtime отличаются от UCRT64. Перед обеими точками сборки добавлено обновление существующих копий этих DLL; раскладка FFmpeg использует для них UCRT64. Проверены обновление, повторный вызов, сохранение кодеков и ошибки. Требуется Windows-прогон check_cc.bat: cc1 после обновления, повтор команды Ninja и сборка Opus должны завершиться с кодом 0.

[ ] ChatGUI Windows: обновление остальных DLL приложения после обновления MSYS2 — 05.10.2026: в cc-probe.log также отличаются Qt и другие DLL. tools/chatgui/build.sh пропускает раскладку при наличии основного комплекта, без сравнения версий; рекурсивная раскладка копирует только отсутствующие зависимости. Runtime GCC обновляется отдельно; для остальных DLL нужно продумать обновление с сохранением приоритета собственной сборки FFmpeg и проверить запуск приложения.

[ ] Android: время жизни config provider — 03.10.2026: utun_bridge_init() передаёт адрес локального utun_config_provider_t в utun_config_set_provider(), который хранит указатель. После возврата из init он недействителен; перед использованием cfg_get_* обеспечить постоянное хранение provider.

[ ] uasync: ложные ERROR об утечке перед cleanup — 01.10.2026: test_dm_e2e и существующий test_conn_mgr проходят, но uasync_destroy() пишет Timer leak до освобождения отменённых heap/immediate таймеров. После cleanup счётчики сходятся, финальная проверка проходит. Перенести предварительные счётчики в DEBUG; ERROR оставить для реального расхождения после cleanup, отдельно показывать активные и отменённые таймеры.

[ ] Общий лимит check.sh после clean — 01.10.2026: расчёт по времени предыдущего прогона дал 205с вместе с полной сборкой и оборвал проверку после успешных тестов до test_uasync_socket_race. Повторный запуск на готовой сборке с большим лимитом прошёл за 206с, включая proxy/burst/load. Разделить бюджет сборки и запуска тестов либо учитывать clean при расчёте.

[ ] Паузы аудио Qt при звонке с SM — 01.10.2026, звонок 939a3ea36f3eeac3, прямой LAN TCP 192.168.40.250:44604 ↔ 192.168.40.247:44173. Tcpdump: общие паузы входящего трафика до 645мс; ping до роутера в среднем 100мс, до SM 128мс. ПК на 2.4ГГц, SM на 5ГГц. Одновременно CallAudio: cb падает до 20–40/с вместо 100/с, буфер Qt растёт до 2с, на SM повторяется refill. В 8с strace аудиопоток продолжает обслуживать PulseAudio без долгих syscall; причина редких аудиоколлбэков пока не установлена. Добавить измерения интервалов и длительности callback, кадров capture/playback и сводку CALL_STATS; сравнить сетевые задержки после подключения ПК к 5ГГц или кабелю. Повторный звонок b2a423ed2d36ed83 (01:25:31–01:27:39): DIRECT сохраняется, RTT 2–3мс; в начале на SM gap tone длится 1.8–2.0с примерно каждые 6с. На Qt частота callback падает вплоть до 4/с, входящий PCM продолжает приходить: буфер 65→465→1465→2079мс за 3с, затем растёт до нескольких секунд, dropped=0. Локализовано прекращение обслуживания duplex-аудио Qt; ожидание backend и задержка обработки PCM пока не разделены. Часы SM отстают от Qt примерно на 1.24с. Добавлена диагностика backend/устройств, времени между callback и длительности capture/playback, фактической частоты callback и PCM кадров за интервал. Счётчики сбрасываются до старта устройства; нужна повторная запись с новой сборкой.

[ ] Оставшаяся задержка запуска аудио SM — после обновления 01.10.2026, звонок 2b9ad4e21a54d8ba: стартового gap tone нет на обеих сторонах; Qt стабильно работает около 100 callback/с, max_idle=15.3мс, буфер 55мс, RTT=2мс. На SM первый capture появляется через 1140мс от startAudio (1164мс от ACCEPT). router.start=781мс, получение снимка устройств=0мс, AudioRecord create=24мс, startRecording=7мс; от начала startRecording до первого кадра=328мс. Capture worker стартует раньше playback, но общий выигрыш пока не виден. AudioTrack create=21мс, play=327мс; система сообщает AudioPortCache contention=497мс и восстановление AudioTrack при setPreferredDevice. Разделить замеры регистрации callback, BT-профиля и применения маршрута; отдельно проверить первый AudioRecord.read. На SM входящий буфер за время подготовки вырос до 1149мс и к концу звонка снизился до 570мс; ускорение старта playback также сократит начальное накопление. Реализован независимый запуск AudioTrack и AudioRecord в play/capture потоках; частичный старт отменяется до освобождения устройств/native. AEC измеряется после готовности обоих. Добавлены callbackMs/btProfileMs/applyMs/refreshMs, длительность первого read и время первой записи playback. Резерв теперь зависит от изменения задержки доставки по timestamps CALL_MEDIA, отдельно от глубины очереди; стартовое накопление и догон не повышают запас. Сохранность FIFO и uint32 wrap проверяются регрессиями. Сборки ядра, Qt и Android после clean успешны; три аудиотеста прошли. Полный check: 113 PASS, 0 FAIL, 1 SKIP (auto_socket_dynamic требует root); proxy/burst/load прошли. Регрессия refill учитывает остаток SoundTouch: два новых кадра могут уже заполнить запас 60мс. SM и Q8 обновлены 01.10.2026 в 03:03. Выигрыш по времени требует пробного звонка с новой версией.

[ ] Гонка invite-файла в test_chat_join_e2e — wait_file() проверяет только существование файла, а wf() создаёт его до записи. Читатель иногда получает пустую ссылку и ошибку префикса utun://. Публиковать готовый файл атомарно.

[ ] Некорректное время в выводе check-proxy — успешные basic_1mb и stress_sessions иногда печатают огромные положительные/отрицательные миллисекунды, хотя итоговая скорость и длительность передачи выглядят нормально. Проверить расчёт вывода.

[ ] Linux ONNX Runtime: повторная регистрация схем при запуске теста — 03.10.2026: test_silero_vad при загрузке модели печатает серии Schema error: ... already registered. Инференс, речь, тишина и reset проходят; отдельно проверить состав и загрузку Linux-библиотек ONNX. Диагностика: /tmp/vad-compressor-silero-tests.log; Android-сборка таких сообщений не содержит.

[ ] test_tcp_proxy_full: некорректное время в диагностике — 03.10.2026: полный check.sh проходит, но строки basic/stress/concurrent/half_close выводят огромные или отрицательные миллисекунды (например, -1611883348002232012ms). Отдельно проверить формат/аргументы вывода длительности; /tmp/vad-compressor-check.log.

[ ] debug_parse_config: формат и индексы категорий — parser ожидает :, документация описывает =; преобразование битовой маски осталось после перехода enum на индексы. Разбор.

[ ] DM: прямой p2p чат с любым пользователем группы — отдельная DM-подсистема. ТЗ и архитектура: /doc/dm_arch.md. Статус: реализовано (dm_core/dm_mailbox/dm_crypto), тесты test_dm (крипто) и test_dm_e2e (интеграция). Остались задачи ниже.

[ ] Звонок (P2P аудио) — голос 1:1, из CHAT-группы или p2p-чата (DM). Отдельный каталог src/call/. Сигналинг + conn_mgr + аудиопоток (Opus) с backpressure (локальный буфер отправки при переполнении роутера) + адаптивный джиттер-буфер (SoundTouch, ускорение до 1.6x) + тоны (глитч/завершение) + watchdog 20с. Android: переключение источника (трубка/громкая/гарнитура). Архитектура: /doc/chat_call_arch.md.

[ ] **DM-звонки** (fallback `group_id==0`) — не сделано: `call_is_online`/`call_start`
    требуют реальную группу (`topo_groups_find`), для DM (group_id==0) маршрута нет.

Рефакторинг: per-instance chat/dm (однопоточный test_dm_e2e)

[ ] BGP-гонка: дропнутый REQUEST_TABLE (отдельная задача, как договорились).

**Место:** `topo_group_new_conn()` в `src/routing_layer/topo_group.c:571`. Дедуп-ветка
«conn already in senders_list» (строки 579–587) делает `return` БЕЗ повторной отправки
`topo_group_send_table_request()`.

**Зачем дедуп:** тот же `ETCP_CONN` стреляет `ETCP_CONN_STATUS_UP` дважды (UDP-линк,
затем TCP-линк). Повторная обработка задваивает `active_conn_count`, из-за чего
переподключение не стартует. Поэтому `return` в дедупе — правильный.

**В чём гонка:** REQUEST_TABLE — это то, что заставляет пира отдать свою таблицу
(NODEINFO/узлы группы). Сценарий «разнесённого старта»:
1. A↔C соединение поднято. A шлёт C `REQUEST_TABLE` для группы G (`topo_group_new_conn`
   → `topo_group_send_table_request`, `topo_group.c:602`).
2. У C группа G ещё **не создана** (создаётся позже, по мере загрузки каналов/синка).
   C получает `REQUEST_TABLE`, но `topo_group_handle_request_table` не находит G →
   дропает запрос.
3. C наконец создаёт G (`topo_groups_create_group` → обход connections →
   `topo_group_new_conn(G, conn_A)` на стороне C) → C шлёт A свой `REQUEST_TABLE`.
   A отвечает своей таблицей → **C узнаёт A**.
4. Но A больше не шлёт C `REQUEST_TABLE` → **A так и не узнаёт C** в группе G.
Итог: при разнесённом старте A не видит C в CHAT-группе (односторонняя видимость).

**Фикс (предполагаемый):** в дедуп-ветке не просто `return`, а предварительно
идемпотентно `topo_group_send_table_request(group, conn)`:
```c
if (((struct TOPO_GROUP_CONN_ITEM*)se->data)->conn == conn) {
    topo_group_send_table_request(group, conn);   /* повторно, идемпотентно */
    DEBUG_INFO(..., "conn already in senders_list, re-request table (%s)", conn->log_name);
    return;
}
```
`topo_group_send_table_request` не ведёт состояния — просто шлёт REQUEST_TABLE,
пир отвечает снимком таблицы (идемпотентно). Лишний дубль таблицы безвреден.

**Замечание:** в однопоточном `test_dm_e2e` (и `test_chat_join_e2e`) из-за
детерминированного порядка группа успевает создаться до прихода REQUEST_TABLE —
поэтому тест проходит. Реальная гонка остаётся для асинхронных сетей. Отдельный
воспроизводимый тест для этой гонки пока не написан.

[ ] «packet undecryptable» на линках C (вторичное). A, узнав B через BGP, через topo_group_connect пытается поднять прямой A↔B линк (seskey 4777737b), но B не настроен принимать A → crypto-шум каждые ~1с. Разобраться с авто-подключением (не пытаться соединяться с узлами, для которых нет [client]/ncd-конфигурации, либо обрабатывать неудачу без спама).

[ ] lwip/proxy: утечка pcb в FIN_WAIT_2 — tcp_shutdown(pcb,0,1) (half-close по FIN от exit) не ставит TF_RXCLOSED, поэтому таймаут FIN_WAIT_2 в tcp_slowtmr не срабатывает: если браузер никогда не пришлёт FIN, pcb навсегда остаётся в FIN_WAIT_2 (утечка, не crash). Двойной free по TIME_WAIT уже закрыт (detach по локальному закрытию + guard abort).

Открытые флаки

[ ] test_etcp_reconnect — флаки под параллельной нагрузкой make check -j4: phase 4 (reconnect после server restart) таймаутит (sent=480 recv=0), при одиночном запуске стабильно TEST PASSED. Наш код (chat_join/chat_sync) его не трогает.