Files
utun2/src/routing_layer/etcp_router_doc.md
T
evgeny e3caf15f32 Guard routing loops and reconcile blocked transit against current paths
Track visited nodes outside the signed body for all routed packet types. Recheck queued transit routes without losing FIFO priority, reject unknown-group transit, and suppress BGP announcements caused only by RTT jitter.

Document routing rules and wire format. Validate graph churn, REINIT, encrypted transit, malformed paths, loop drops and queue cleanup; full suite: 106 passed, 1 skipped, plus proxy integration and sanitizer checks.
2026-09-28 15:36:47 +03:00

19 KiB

ETCP Router: протокол и проверка транспорта

Логическое соединение определяется (group_id, remote_node_id, svc_id). Роутер обеспечивает порядок, дедупликацию и повторную отправку между конечными узлами; физический ETCP и транзитные узлы могут меняться. Формат изменён без обратной совместимости.

Wire-формат

Packed SVC_ROUTE_HDR, 57 байт:

cmd:1 | group:8 | dst:8 | src:8 | seq:4 | svc:1 | flags:1 | timestamp:2 | reset_id:8 | peer_reset_id:8 | challenge:8

Числовое представление полей сохраняет существующий формат проекта (native endian). Отдельной версии/согласования формата нет; узлы должны обновляться совместно.

Полный пакет: [SVC_ROUTE_HDR][payload][signature?][visited_ids:8*count][count:1]. Источник начинает список своим ID; транзит проверяет отсутствие собственного ID и дописывает его. Правило действует для DATA, ACK и всех управляющих пакетов. Повторное посещение отбрасывается; также проверяются размер, уникальность ID, первый ID=src и последний ID=ETCP peer. Предел — 64 ID; на конечном узле полный список допустим, для следующего перехода места уже нет. Список находится вне end-to-end подписи/шифрования и снимается перед crypto decode. Это защита от временных петель при сходимости BGP, а не криптографическое подтверждение пути.

Маршруты строит BGP по правилам topo_group.h. Транзит использует текущий next hop. При backpressure пакет ждёт в очереди; waiter и таймер 20 ms повторно проверяют маршрут. Смена пути не требует освобождения старой физической очереди. ID добавляется один раз на приёме, а не при каждой попытке выгрузки. При исчезновении маршрута транзит отбрасывается; доставку повторяет источник.

reset_id — случайный ненулевой ID локального экземпляра соединения; peer_reset_id — ID получателя. DATA, ACK и CLOSE принимаются только при совпадении обоих ID с текущей парой. Смена физического транспорта пару не меняет.

Пакет Флаги Payload Поля
HELLO START (0x80) нет собственный ID, peer=0, challenge=0
CHALLENGE RST (0x40) нет оба ID, случайный ненулевой challenge
CONFIRM START RST нет
DATA 0 либо SIGNED/ENCRYPTED непустой seq, оба ID, challenge=0
ACK 0 нет seq=следующий ожидаемый, оба ID, challenge=0
CLOSE CLOSE (0x02) нет оба ID, challenge=0

Незнакомый ID сам по себе не сбрасывает рабочую сессию. Получатель выдаёт свежий challenge и меняет peer ID только после соответствующего CONFIRM. CHALLENGE не меняет рабочую сессию: отправляется CONFIRM и, если нужно, встречный challenge. Потерянные сообщения handshake восстанавливаются повторением HELLO через 300 ms; выдача новых challenge ограничена тем же интервалом.

Первичное согласование сохраняет накопленные исходящие данные. Подтверждённый рестарт пира сбрасывает seq/очереди и уведомляет сервис; собственный ID сохраняется. Явный etcp_router_conn_restart создаёт новый собственный ID. Сброс выполняется до уведомления, поэтому данные, отправленные сервисом из callback, сохраняются. Сервис должен заново выполнить прикладную синхронизацию после уведомления; гарантии доставки не продолжаются через явный сброс сессии.

Challenge защищает от случайных старых/переставленных управляющих пакетов, но не является end-to-end аутентификацией против злонамеренного транзитного узла. Управление опирается на защиту физических ETCP-соединений. Подпись/шифрование DATA включаются вызывающим сервисом через ROUTE_CRYPTO_SIGN / ROUTE_CRYPTO_ENCRYPT.

Очереди и подтверждения

  • send_q: plaintext + seq + crypto mode, до 64 пакетов при обычной отправке. Кодирование происходит при отправке после handshake, с актуальной парой ID.
  • tx_seq: следующий выделяемый seq; tx_sent: граница фактически отправленного; tx_acked: граница подтверждённого.
  • inflight_q: неизменяемое тело с подписью, без списка посещённых узлов, обычно до 256 пакетов. Каждый ретрансмит начинает новый список; ошибка не теряет pending-пакет.
  • ACK принимается только в модульном интервале (tx_acked, tx_sent]. Дубликаты, старые и выходящие за границу ACK не продлевают ожидание прогресса. Удаляются существующие inflight-записи, а не перебираются все номера до произвольного ACK.
  • Новые DATA и ретрансмиты идут по одному через waiter физической send_input_q. При смене пути отменяется waiter на очереди, где он действительно был зарегистрирован, включая уже запланированный callback.
  • Каждые 300 ms без прогресса планируется проход повторной отправки. После 17 попыток без ACK соединение закрывается. Отсутствующий маршрут проверяется каждые 20 ms; очередь сохраняется. Пока первичный handshake не завершён, HELLO повторяется без отдельного таймаута.
  • Watchdog 500 ms восстанавливает отправку и переносит waiter при смене заблокированного пути.
  • Сброс физического ETCP сохраняет router inflight и seq. Восстановление самого ETCP-handshake остаётся обязанностью транспортного слоя.
  • Crypto decode/проверка подписи выполняются до помещения в reorder и изменения rx_seq. Повреждённый пакет не подтверждается как доставленный.
  • DATA выдаются сервису строго по seq; входящие дубликаты повторно проверяются после извлечения из очереди.
  • Wire-пакет ограничен PKTNORM_MAX_DGRAM_SIZE (16384), включая заголовок, crypto overhead и резерв полного списка переходов. Пустой payload запрещён: он используется для управления.

Сервисный API

etcp_router_conn_send(rconn, data, len, mode) копирует payload; 0 означает принятие в очередь, а не end-to-end доставку. При backpressure производитель ждёт через etcp_router_on_send_ready; отменяет ожидание через etcp_router_cancel_send_ready. Handle должен жить до callback/отмены. Режим force низкоуровневого etcp_route_send обходит порог очереди и не предназначен для непрерывного потока.

etcp_router_bind регистрирует callback, владеющий переданным ll_entry. Формат доставки — 26 байт заголовка:

svc:1 | src:8 | dst:8 | rx_flags:1 | group:8 | payload...

Читать невыравненные числовые поля нужно через memcpy. Заголовок без payload означает уведомление о закрытии/сбросе. После обработки освобождаются queue_dgram_free(entry) и queue_entry_free(entry).

Callback может закрыть соединение. Освобождение send_q отложено за пределы callback производителя; отправка проверяет, не закрылся/перезапустился ли rconn. При уничтожении реестр удаляется ровно один раз для каждого соединения.

Проверки и диагностика

test_bgp_paths использует настоящий BGP и роутер в управляемой сети четырёх узлов: все 64 графа, разбиение/слияние, изменения при непустых очередях, REINIT, выбор кратчайшего пути с устойчивым разрешением равенства. Дополнительно проверяет подписанный зашифрованный транзит, петли DATA/ACK/control, повреждённые списки переходов, смену заблокированного next hop и очистку таймеров. Счётчики UTUN_INSTANCE.router_loop_drops и router_path_errors сопровождаются WARN с идентичностью пакета.

tests/test_etcp_router_faults перехватывает SVC_ROUTE между настоящими роутерами, очередями и UASYNC-таймерами. Физическая очередь тестовая: нижележащий ETCP не скрывает потери от роутера. Проверяются:

  1. Потери DATA/ACK, перестановка, дубликаты, четыре потока (два направления × два сервиса), backpressure и ограниченность физической очереди.
  2. Потеря HELLO/CHALLENGE/CONFIRM в обоих направлениях.
  3. Старые, повторные, слишком большие ACK и ACK ещё не отправленного пакета.
  4. Сохранение inflight при сбросе транспорта.
  5. Старые DATA/ACK/CLOSE/HELLO/CONFIRM после смены сессии.
  6. Переполнение uint32 seq.
  7. Закрытие после смены пути при зарегистрированном и отложенном waiter.
  8. Закрытие из callback приёма.
  9. Смена маршрута при занятой старой очереди без новой отправки.
  10. Исчезновение и восстановление маршрута.
  11. Таймаут при непрерывных повторных ACK без прогресса.
  12. Одновременный рестарт обоих концов.
  13. Граничные размеры payload, crypto overhead, SIZE_MAX.
  14. Закрытие из callback производителя при освобождении send_q.

Успех передачи требует совпадения содержимого, порядка и количества, финальных ACK, пустых send/inflight. Завершение проверяет освобождение таймеров. Unit-набор включает 34 проверки, в том числе криптографию и повторную доставку после неверной подписи.

Сетевой test_etcp_router использует UDP/dummynet, backpressure и сброс с восстановлением ETCP-handshake. test_etcp_router_reconnect проверяет a–b–c и перезапуск b, по 500 пакетов до/после, с ожиданием последних ACK в каждой фазе и фиксированным seed.

Переменная ROUTER_TEST_LOG=/tmp/router.log включает подробный файл. Рабочая категория — etcp_route; DEBUG показывает handshake, пару эпох, ретрансмиты и смену состояния; поштучные ACK/DATA оставлены на TRACE. Ошибочные ветки не скрыты. В fault-тесте WARN/ERROR для намеренно неверных ACK/размеров и искусственного таймаута ожидаемы.

Предыдущий этап проверки роутера: 14 fault-сценариев и 34 unit-проверки прошли; fault-набор также прошёл ASan/UBSan/LeakSanitizer (инструментированы router, route_crypto и ll_queue, остальная библиотека обычная). UDP-тест: 440/440 доставлено и подтверждено. Транзитный: 1000/1000 доставлено и подтверждено. Это не доказательство отсутствия всех ошибок стека.

Lifecycle транспорта и восстановление

ETCP имеет единую точку финального освобождения после DELETE, завершения callback и освобождения последней ссылки. Запрос закрытия идемпотентен и сразу запрещает новые send/ref_take.

NCD-entry удерживает одну ссылку ETCP. При DELETE запись сначала исключается из реестра, затем владельцы получают CLOSED; старые handles безопасно закрываются после создания нового соединения с тем же node_id. Закрытые callbacks/handles удаляются после внешней рассылки. Отложенный начальный UP отменяется при DOWN/close. При удалении сокета обход использует снимок ссылок ETCP, поскольку DOWN может удалить NCD-entry.

Recovery удерживает TOPO_NODE для каждого уникального кандидата и освобождает ссылку при любом завершении. При успехе BGP сначала получает собственный NCD-handle. test_topo_recovery удаляет B и проверяет самостоятельное подключение A↔C и доставку 1000 сообщений без тестового reconnect.

etcp_session.c согласует эпохи физического транспорта через HELLO/CHALLENGE/CONFIRM поверх аутентифицированного UDP/STCP. Каждый DATA/ACK содержит пару эпох; старые кадры отбрасываются до обработки. Сброс потока не генерирует ложный DOWN. REINIT повторно запускает обмен таблицами BGP. Формат и таймауты описаны в doc/etcp_protocol.txt.

Дополнительные проверки:

  • test_etcp_lifecycle: DELETE/refcount, повторное закрытие, закрытие из callback, FIN_WAIT, отмена начального UP, удаление сокета, владение recovery.
  • test_etcp_session: реальные crypto/ingress/таймеры; reset любой стороны, одновременный reset, потери всех видов control, повторный reset, отказ одного из двух линков, timeout, старые DATA/ACK/control, неверные размеры.
  • test_etcp_router_tcp: тот же контроль содержимого, порядка, дубликатов, backpressure и финальных ACK по STCP; обе стороны сбрасываются одновременно.

Диагностика по запросу: SESSION_TEST_LOG=/tmp/session.log, LIFECYCLE_TEST_LOG=/tmp/lifecycle.log, ROUTER_TEST_LOG=/tmp/router.log, BGP_TEST_LOG=/tmp/bgp.log. ROUTER_TEST_RESET=client|server|both выбирает сторону reset; ROUTER_TEST_TCP=1 включает STCP. Ожидаемые ошибки отрицательных сценариев не скрываются.

Дополнительно исправлен монитор conn_mgr: наличие маршрута через посредника не означает прямого соединения. Фоновый PING выбирает транспорт по node_id конечного peer и живой линк именно этого транспорта. Проверка BGP-треугольника выдерживает полный период фоновых проб и требует отсутствия ошибок входящих пакетов до внесения потерь. На старом monitor эта проверка воспроизводимо падает; с исправлением проходит.

Итоговая проверка текущих исправлений: 16 регрессионных executable-тестов прошли (router unit/faults/lifecycle/session, UDP, TCP, reconnect, recovery, STCP traffic, NCD, три conn_mgr, normalizer/ETCP, BGP triangle). Семь наборов дополнительно прошли ASan/UBSan/LeakSanitizer: lifecycle, session, router faults, UDP router, TCP router, autonomous recovery, BGP triangle. Инструментированы ETCP core/API/connections/session, NCD, router, recovery, topo_group/connect, conn_mgr core/monitor и ll_queue; остальные библиотечные объекты обычные. Отрицательные тесты намеренно вызывают ошибки; после принудительного удаления транспорта возможен отказ приёма запоздалой UDP-датаграммы без существующего линка. Финальный BGP-прогон, включая фоновые пробы и cleanup таймеров, завершился без диагностических ошибок.