You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

18 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. Исходное падение отдельного root-теста и последующая доработка описаны ниже. После исправления autosockets общий набор дал 97 PASS / 1 SKIP, а check-proxy, check-burst, check-load прошли: /tmp/root-fix-full-check.log.

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. Исправленный тест создаёт собственный network namespace, поднимает loopback и добавляет default route только клиентским dummy. Фильтр остаётся включённым; отдельно проверяется исключение серверного dummy, иначе его сокет скрывал бы полный обрыв клиентской сети.

Кроме того, тест сохраняет handle общего дедлайна, traffic_send и traffic_monitor в одну переменную timeout_handle; фазовые таймеры вообще не сохраняются. При выходе не отменялись все таймеры; зафиксирован остаток 9 таймеров (665/656). В обработчиках трафика освобождался entry без queue_dgram_free. Теперь у каждого таймера свой slot, waiters отменяются до teardown, payload освобождается, deferred cleanup завершается до проверки баланса таймеров, сокетов и u_get_allocated_count().

Новый генератор шлёт 1024-байтовые пакеты независимо в обе стороны, по одному через threshold waiter, с бюджетом 64 пакета на tick. Проверяются содержимое, строгий seq, ограниченность TX/RX очередей, реальное срабатывание backpressure, остановка генераторов при полном обрыве и полное опустошение после восстановления. NCD handle удерживает логическую сессию: смена интерфейса не является разрешением терять её данные. Остатки после действительно удалённого соединения — отдельный допустимый случай.

Прогон нагрузки обнаружил ошибки, скрытые прежними проверками:

  • tcp_socket_add не сохранял netif_index: монитор принимал interface-bound сокет за сокет default route и менял его адрес при событиях другого интерфейса.
  • Удаление TCP модели искало её по имени уже удалённого интерфейса и порту из БД. Теперь запись интерфейса хранит имя и прямую ссылку на модель; один путь закрывает listener, линки и модель. Портовые записи удаляются с сохранённым именем.
  • tcp_socket_remove не закрывал линки, созданные вне NCD. Теперь закрывает все связанные линки до освобождения модели; root-тест создаёт такой линк отдельно.
  • Поиск UDP модели не исключал TCP. Добавлена проверка типа сокета.
  • При selective ACK sender мог выдавать новые seq за границей MAX_INFLIGHT_SIZE относительно cumulative ACK. Теперь генерация seq останавливается на границе, cumulative ACK возобновляет очередь даже при отсутствии уже selectively-acked записей. Тесты проверяют блокировку, возобновление и uint32 wrap. ACK не может подтвердить ещё не назначенный seq; декодирование старшего байта seq/ACK не делает signed shift.
  • Timestamp Q16 умножался как signed int и переполнялся на циклических/отрицательных смещениях. Перевод выполняется в uint32. Jitter раньше всегда получал нулевую разницу: предыдущий RTT читался после перезаписи; теперь берётся до неё, EWMA использует int64.
  • UBSan выявил невыровненные canary/указатели в mem.c и memory_pool.c. Метаданные читаются/пишутся через memcpy; покрыты размеры блоков 1..17 и повторное использование пула.
  • Повторный общий прогон поймал test_etcp_connect: PID-порты пересекались с занятым UDP-портом (EADDRINUSE). Теперь сервер bind-ится на порт 0, клиент получает фактический порт через getsockname; собственного алгоритма поиска свободного порта нет.

Root ASan/UBSan/LSan: PASS, 153335 / 175912 пакетов, reinit=0, все очереди пусты, баланс памяти/таймеров/сокетов совпал (/tmp/root-fix-asan.log). Инструментированы тест, ETCP/session/router/NCD/autosocket/STCP/normalizer, очереди, event loop и аллокаторы; прочие объекты архивов обычные. Unit-логи: /tmp/root-session-asan.log, /tmp/root-memory-asan.log. LSan запускается вне sandbox: под ptrace он не работает.

Финальная чистая обычная сборка: ./check.sh — 97 PASS / 0 FAIL / 1 SKIP; proxy, burst и load — PASS (/tmp/root-fix-final-check.log). Отдельный root-тест — PASS, 188069 / 191552 пакета, все очереди пусты, reinit=0, баланс ресурсов совпал (/tmp/root-test-final.log). После добавления drain отложенной очистки test_etcp_connect повторно прошёл без остаточных таймеров (/tmp/root-connect-final.log).

Подробные socket/connection INFO включаются только через UTUN_TEST_DEBUG=1. ENETUNREACH непосредственно при принудительном удалении IP/интерфейса ожидаем до обработки netlink; тест требует последующей доставки всех данных. Ошибки выхода за receive window недопустимы и после исправления sender gate не наблюдаются.

Попутная проблема диагностики

lib/debug_config.c:debug_parse_config() требует :, хотя документация описывает =. Кроме того, функция всё ещё вычисляет индекс через битовую маску, хотя категории теперь являются индексами. Исправление в этот набор не включено; тесты используют debug_set_category_level() напрямую.