20 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 не скрывает потери от роутера. Проверяются:
- Потери DATA/ACK, перестановка, дубликаты, четыре потока (два направления × два сервиса), backpressure и ограниченность физической очереди.
- Потеря HELLO/CHALLENGE/CONFIRM в обоих направлениях.
- Старые, повторные, слишком большие ACK и ACK ещё не отправленного пакета.
- Сохранение inflight при сбросе транспорта.
- Старые DATA/ACK/CLOSE/HELLO/CONFIRM после смены сессии.
- Переполнение uint32 seq.
- Закрытие после смены пути при зарегистрированном и отложенном waiter.
- Закрытие из callback приёма.
- Смена маршрута при занятой старой очереди без новой отправки.
- Исчезновение и восстановление маршрута.
- Таймаут при непрерывных повторных ACK без прогресса.
- Одновременный рестарт обоих концов.
- Граничные размеры payload, crypto overhead, SIZE_MAX.
- Закрытие из 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 таймеров, завершился без диагностических ошибок.