12 KiB
Разбор полного прогона 2026-09-27
Исходный результат ./check.sh вне песочницы: 92 passed, 5 failed, 1 skipped (98 тестов).
Песочница запрещает сетевые сокеты; её результаты не использовались для оценки протокола.
Промежуточный прогон: 96 passed, 1 failed, 1 skipped; оставался test_chat_join_e2e.
После KEY_REGISTER_ACK и исправления невыровненных чтений повторный полный
make check -j4: 97 passed, 0 failed, 1 skipped (98).
Лог: /tmp/utun-ack-final-check.log. Пропущенный test_auto_socket_dynamic
запущен отдельно с root в изолированной сети и упал — подробности ниже.
Внешние check-proxy/check-burst/check-load не выполнялись: цепочка root-проверок
остановилась на этом тесте.
test_etcp_lifecycle, test_etcp_link_stress, test_conn_mgr_phases дополнительно
прошли ASan/UBSan/LeakSanitizer. Инструментированы тесты и изменённые транспортные/
маршрутные модули, остальные объекты библиотек взяты из обычной сборки.
Логи: /tmp/ncd-late-asan.log, /tmp/link-role-asan.log, /tmp/cm-phases-asan.log.
Исправленные причины
test_conn_mgr_phases: после TIMEOUT фазы DIRECT NCD навсегда подавлял UP, хотя conn_mgr сохранял handle для REVERSE. В логе физический ETCP уже UP, но conn_mgr продолжал DIRECT_REQ до своего таймаута. TIMEOUT теперь обозначает истечение срока первой попытки, CLOSED — окончательное завершение. Удерживаемый handle получает поздний UP. Отдельный lifecycle-регрессионный тест воспроизводит TIMEOUT → INIT/UP (одно уведомление) → DOWN → UP; до исправления он падал.test_etcp_link_stress: ожидал reset всей сессии при коллизии дополнительного линка. Проверка заменена на сохранение эпох, отсутствие reset и дублей линков, доставку и ACK сообщений через новый линк. Прежний тест забирал сырые фрагменты вместо normalizer и мог считать служебные пакеты полезным трафиком. Теперь он использует отдельный ETCP service и проверяет каждый байт и порядок сообщений. Эта проверка выявила ошибку реализации: при принятии встречного INIT сохранённый исходящий линк не менялis_server. Роль обновляется при отправке INIT_RESPONSE.test_bgp_route_exchange: после разрыва B–C recovery успешно строил C–A, нарушая предположение теста о фиксированной цепочке. Теперь A и C принимают только ключ B: все прежние проверки withdraw/restore сохранены, обход запрещён условиями тестовой сети. Автоматический обход отдельно проверяетtest_topo_recovery.test_tcp_io: адрес 192.0.2.1:81 в окружении устанавливал TCP-соединение, поэтому connect timeout закономерно не срабатывал. Теперь используется локальный listener с заполненной accept-очередью, проверяется ETIMEDOUT. Перед уничтожением uasync тест завершает отложенное освобождение TCP и проверяет баланс таймеров.
Диагностика тестов включается UTUN_TEST_DEBUG=1; обычный запуск не включает DEBUG.
Остаточные DATA после удаления соединения могут быть undecryptable и сами по себе
не означают ошибку транспорта.
Исправлено: гонка регистрации приглашения
test_chat_join_e2e, happy path A != C, падал из-за отсутствия подтверждения KEY_REGISTER.
Раньше chat_invite_build_link() вызывал chat_join_register_key() и сразу возвращал ссылку.
Успех etcp_route_send() означает постановку в очередь. Он не подтверждает обработку
ключа посредником C. Параллельное подключение J–C может завершиться раньше доставки A–C.
Доказательство: /tmp/join-e2e-debug.log, один ключ 90c4b081833450a0:
- 01:39:02.437851 — A пишет
join-key registered, фактически ключ только поставлен в отправку. - 01:39:02.446527 — C отвергает JOIN_INFO_REQ: ключ ещё неизвестен.
- 01:39:02.449724 — C сохраняет этот ключ, но J уже получил терминальный ERROR.
Теперь C отвечает KEY_REGISTER_ACK только после сохранения ключа. A сопоставляет channel/target/key и выдаёт ссылку после успешного ACK и локального сохранения ключа. Отрицательный ACK завершает запрос ошибкой. Чужие, неподписанные, незашифрованные, повторные и запоздалые подтверждения не могут завершить другой запрос.
Контракт асинхронных API chat_join_register_key / chat_invite_build_link:
NULL означает ошибку запуска без callback; принятый запрос даёт ровно один callback.
Обычное завершение происходит после возврата API, включая self. Cancel/destroy
завершают запрос синхронно с CANCELLED. Handle недействителен после callback.
Для удалённого узла дедлайн 5 секунд; доставку и повтор потерянных пакетов обеспечивает
надёжный router. Повторная регистрация того же ключа тем же инвайтером обновляет TTL;
замена инвайтера для существующего ключа запрещена. Отмена может оставить ключ на C
до истечения TTL, но A не авторизует по нему вход, поскольку локально такой ключ
сохраняется только при успешном завершении.
GUI получает request_id в событии и игнорирует устаревшие результаты. Новый запрос отменяет предыдущее ожидание GUI. Headless связывает ответ с JSON id и отменяет все ожидания отключившегося клиента. Ни один из путей не запускает вложенный event loop.
Дополнительные исправления:
- Устранён double-free при ошибке
etcp_route_send: пакет уже освобождён роутером. - Headless прекращает разбор пакета после
quit; последующая команда не создаёт запрос на уже закрытом клиенте. При destroy отменяются cleanup_timer и запросы, удаляются клиентские сокеты и освобождается контекст. - UBSan обнаружил невыровненное чтение group_id из
d + 1в chat_sync. Оно и три аналогичных диагностических чтения заменены на memcpy. - Тест chat_join теперь останавливает вручную запущенный member_sync; проверяется баланс выделения/освобождения таймеров.
Проверки:
test_chat_join: 31/31, включая задержанный/отсутствующий ACK, таймаут, неверные channel/peer/key/flags/размер/status, дубликат, отказ хранения из-за конфликта инвайтера, отмену, shutdown, параллельные ключи и ошибку отправки.test_chat_join_e2e: 3/3 (A≠C, A=C, неверный ключ). C принудительно откладывает регистрацию на 200 мс; проверяется отсутствие ссылки до обработки регистрации. Дополнительно проверяются подавление старого GUI-запроса и реальный headless control socket: параллельные ответы, валидность ссылок, quit и команда после него.- ASan/UBSan: оба теста проходят. LeakSanitizer и баланс таймеров проверены в
test_chat_join; E2E использует fork/_exit, поэтому сам по себе не проверяет утечки
при завершении дочерних процессов. Инструментированы изменённые chat-модули и
transport/routing, остальные объекты взяты из обычной библиотеки.
Логи:
/tmp/utun-ack-join-asan.log,/tmp/utun-ack-e2e-asan.log.
Отдельный root-тест auto_socket_dynamic
Запуск в sudo unshare --net с поднятым loopback закончился TIMEOUT через 30 секунд.
Лог: /tmp/utun-ack-root-checks.log. Это не PASS основного набора: в нём тест пропущен.
В конфиге теста включён auto_sockets, но не отключён стандартный фильтр
auto_socket_skip_no_default_route=1. Dummy-интерфейсы теста не имеют default route,
а auto_socket.c:reconcile_iface() исключает такие интерфейсы. Лог показывает
no links created, затем TIMEOUT. Для локального сценария тест должен явно задать
auto_socket_skip_no_default_route=no и проверить создание сокетов до трафика.
Кроме того, тест сохраняет handle общего дедлайна, traffic_send и traffic_monitor в одну переменную timeout_handle; фазовые таймеры вообще не сохраняются. При выходе не отменяются все таймеры; зафиксирован остаток 9 таймеров (665/656). В обработчиках трафика освобождается entry без queue_dgram_free. Нужна отдельная доработка fixture: владение таймерами и payload, backpressure отправителя, завершение deferred cleanup. Производственный auto_socket в рамках исправления приглашений не менялся.
Попутная проблема диагностики
lib/debug_config.c:debug_parse_config() требует :, хотя документация описывает =.
Кроме того, функция всё ещё вычисляет индекс через битовую маску, хотя категории
теперь являются индексами. Исправление в этот набор не включено; тесты используют
debug_set_category_level() напрямую.