19 KiB
ETCP Router: протокол и сервисный API
Логический канал определяется тройкой (group_id, remote_node_id, svc_id).
Роутер обеспечивает порядок, дедупликацию и повторную отправку между конечными узлами;
физический ETCP и транзитные узлы могут меняться. Работа и callbacks выполняются в uasync-потоке ядра.
Объявления и точные константы — в etcp_router.h, схема потоков — в
архитектурном описании. Формат не имеет согласования версий;
узлы используют один формат сборки, обратная совместимость не поддерживается.
Wire-формат
Packed SVC_ROUTE_HDR, 57 байт, числовые поля в native endian:
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
Полный пакет: [SVC_ROUTE_HDR][payload][signature?][visited_ids:8*count][count:1].
Источник начинает список собственным ID. Каждый транзитный узел проверяет список и добавляет свой ID один раз на приёме, перед ожиданием очереди. При повторной отправке источника список создаётся заново. Проверяются границы, 1..64 ID, отсутствие нулей/дубликатов/собственного ID, первый ID=src, последний ID=ETCP peer. Полный список из 64 ID допустим на конечном узле; следующий транзит уже невозможен.
Список находится вне end-to-end подписи/шифрования и снимается перед crypto decode.
Это защита от временных петель BGP, а не доказательство пути от недоверенного пира.
Транзит не декодирует payload. При занятой физической очереди он сохраняет пакет в FIFO
для (group, src, dst); waiter и таймер 20 ms повторно выбирают путь.
При исчезновении маршрута пакет отбрасывается, повтор доставки выполняет источник.
Отправитель сначала выбирает транспорт через conn_mgr, затем BGP группы; при отсутствии группы допускается прямой глобальный транспорт. Транзит требует маршрута зарегистрированной группы: одно прямое соединение не разрешает ретрансляцию для неизвестного group_id. Право обработки сервисных данных проверяет сам сервис.
Согласование эпох
reset_id — случайный ненулевой ID локального экземпляра канала,
peer_reset_id — подтверждённый ID получателя. DATA/ACK/CLOSE допускаются только для текущей пары.
Смена физического транспорта пару не меняет.
| Исходящий пакет | flags | Payload | Значение |
|---|---|---|---|
| HELLO | START, 0x80 | Нет | Свой ID, peer=0, challenge=0 |
| CHALLENGE | RST, 0x40 | Нет | Оба ID и случайный ненулевой challenge |
| CONFIRM | START|RST | Нет | Оба ID и эхо challenge |
| DATA | 0, SIGNED 0x08 и/или ENCRYPTED 0x04 | Непустой | seq, оба ID, challenge=0 |
| ACK | 0 или RECEIVE_BUSY 0x01 | Нет | seq=rx_seq, оба ID, challenge=0 |
| CLOSE | CLOSE, 0x02 | Нет | Оба ID, challenge=0 |
Незнакомый ID не сбрасывает рабочую сессию: получатель сначала выдаёт challenge. CONFIRM должен совпасть с локальным ID, pending peer ID и challenge. CHALLENGE вызывает ответ CONFIRM и при необходимости встречную проверку, но сам по себе не меняет сессию. Выдача новых challenge ограничена интервалом 300 ms; пока первичный peer не подтверждён, HELLO повторяется каждые 300 ms без отдельного таймаута. При этом исходные данные остаются в send_q. На этом этапе отсутствие маршрута обрабатывается тем же HELLO-таймером.
Первичное согласование сохраняет накопленные исходящие данные.
Подтверждённый рестарт пира очищает seq/очереди/метрики и уведомляет сервис, сохраняя собственный ID.
Явный etcp_router_conn_restart выбирает новый собственный ID. Отсутствующий канал не создаётся.
Сброс выполняется до уведомления, поэтому отправка из callback относится уже к новой сессии.
Гарантии прежнего канала не продолжаются через его сброс; прикладную синхронизацию возобновляет сервис.
Challenge защищает от старых/переставленных пакетов, но не аутентифицирует end-to-end управление против злонамеренного транзитного узла. Управление опирается на защиту физических ETCP-соединений. DATA кодируются через route_crypto: сначала шифрование, затем подпись заголовка и payload.
Очереди и seq
| Состояние | Содержимое/смысл |
|---|---|
| send_q | Plaintext FIFO; в entry.data — выделенный seq и crypto mode |
| tx_seq | Следующий выделяемый seq |
| tx_sent | Граница действительно отправленных пакетов |
| tx_acked | Граница подтверждённых пакетов, модульный uint32 |
| inflight_q | Неизменяемое подписанное/зашифрованное тело без visited-list; исходная длина payload учитывается отдельно |
| incoming_q | Проверенный plaintext между сетью и reorder |
| recv_q | Reorder по seq; данные, ещё не выданные сервису |
| rx_seq | Следующий ожидаемый seq, увеличивается после успешного callback |
Начальные seq равны 0. Сравнения выполняются по модульной разности uint32.
Обычная отправка ограничена 64 пакетами send_q; force обходит этот порог, но не проверку размера/режима.
Кодирование выполняется при реальной отправке после handshake, с текущими эпохами.
После успешного etcp_send запись снимается с send_q; при отказе исходный пакет остаётся в очереди.
Обычный inflight — 256 пакетов, минимум настраиваемого max_inflight — 4. Во время minRTT probe действует лимит 4; сброс сессии возвращает default=256. Новые DATA и ретрансмиты проходят по одному через waiter физической send_input_q. При смене пути отменяется ожидание на очереди, где оно действительно зарегистрировано. Штатный normalizer откладывает физические waiters, чтобы избежать рекурсивного drain.
Wire-пакет ограничен PKTNORM_MAX_DGRAM_SIZE (16384), включая 57-байтный заголовок,
crypto overhead и резерв полного 513-байтного visited-list. Пустой payload зарезервирован для управления.
Входной размер проверяется до выделения/копирования payload, включая SIZE_MAX и неверные биты mode.
ACK, backpressure и повторы
ACK принимается в модульном диапазоне [tx_acked, tx_sent].
Только продвижение seq освобождает существующие inflight-записи и учитывает первую доставку payload.
Старые/выходящие за диапазон ACK не продлевают ожидание прогресса.
Исключение — допустимый RECEIVE_BUSY: даже при прежнем seq он подтверждает живость принимающего сервиса, обновляет отсчёт RTO, обнуляет no_ack_count, останавливает текущий повторный проход и новую отправку. Снятие BUSY возобновляет send_q, в том числе при прежнем seq. Busy ACK не подтверждает отложенные пакеты. c_stale_ack включает и допустимые ACK без продвижения.
receive_ready получает заимствованный проверенный plaintext перед callback и ACK.
Это предикат без I/O и изменения/закрытия состояния роутера.
При 0 пакет остаётся в recv_q, порядок сохраняется; принимающий узел публикует BUSY примерно каждые 100 ms.
CLOSE обрабатывается отдельно и предикатом не блокируется.
etcp_router_resume_receive планирует продолжение, не создаёт канал и не сохраняет аргументы сервиса.
Снятие предиката через set_receive_ready(NULL) само по себе не планирует продолжение уже остановленных каналов.
ACK ограничен снизу интервалом 10 ms. При повторном DATA после потери ACK текущая граница отсылается снова. Idle-таймер 500 ms отправляет ACK только если rx_seq ещё отличается от last_sent_ack_seq; безусловного повтора каждого последнего ACK нет.
Каждые 300 ms без продвижения ACK или допустимого BUSY планируется повторный проход inflight. Канал закрывается после 17 циклов (около 5 s), для ETCP_RT_ID_CALL — 67 (около 20 s). Отсутствующий маршрут после согласования проверяется каждые 20 ms, очередь сохраняется. Watchdog 500 ms повторяет kick при непустой send_q и переносит waiter на актуальный путь. Сброс физического ETCP сохраняет router seq/inflight; восстановление физического handshake выполняет транспорт.
До изменения seq/reorder выполняются требования crypto policy и decode/verify. Повреждённая подпись/криптограмма не подтверждается как доставленная. Приём проверяет окно относительно max_inflight, а суммарный буфер incoming_q+recv_q ограничен max_inflight+1.
Сервисный API и локальная доставка
etcp_router_bind регистрирует callback, получающий владение ll_entry:
svc:1 | src:8 | dst:8 | rx_flags:1 | group:8 | payload...
Заголовок доставки — 26 байт. Числовые поля не выровнены, читать через memcpy. rx_flags содержит crypto-флаги исходного сетевого пакета. Пустой payload — уведомление close/reset. После обработки сервис освобождает queue_dgram_free(entry) и queue_entry_free(entry).
conn заимствован на время callback и может обозначать промежуточного ETCP-пира. Для локальной доставки, закрытия и отложенного приёма он синтетический: instance и peer=self. Этот объект нельзя сохранять или использовать для сетевой отправки/ref_take; реальные end-to-end src/dst/group берутся из сервисного заголовка. Каждый синтетический callback имеет собственный контекст, включая вложенные вызовы разных ядер.
Замена ненулевого callback сохраняет readiness/policy. bind(NULL) и unbind снимают оба ограничения; unbind возвращает -1 при отсутствии обработчика, bind(NULL) в этом случае даёт 0. Снятие обработчика не закрывает существующие каналы.
etcp_router_conn_send копирует непустой payload в сетевую send_q.
Возврат 0 означает принятие, не end-to-end доставку.
Производитель ждёт порог через on_send_ready и отменяет handle через cancel_send_ready.
Callback ожидания может выполниться синхронно; handle и arg должны жить до завершения/отмены.
Отсутствующий канал не создаётся ни проверкой места, ни регистрацией ожидания.
etcp_route_send принимает entry с svc_id перед payload и освобождает его во всех исходах.
Для удалённого узла создаёт канал и ставит копию в очередь.
Для dst=self выполняет callback синхронно без канала, seq/inflight, receive_ready или crypto.
Доверенная self-ветка обходит сетевую crypto policy и передаёт rx_flags=0; допустимые биты mode проверяются.
Её payload — 1..65509 байт, чтобы 26-байтный заголовок и данные помещались в uint16 длину.
Нет обработчика, неверный размер/режим или отказ памяти дают -1 вместо ложного успеха.
Закрытие и метрики
Индивидуальное закрытие сначала снимает канал с реестра, затем уведомляет сервис. Callback может закрыть свою группу/другие каналы или создать замену с тем же ключом. Ресурсы/таймеры очищаются, память канала освобождается отложенно; старый rconn после close использовать нельзя. Сервисные producers/waiters должны быть отменены до массового close_group/destroy.
close_group удаляет логические каналы и ожидающий транзит указанной группы. destroy снимает wire-обработчик, закрывает все логические каналы и очищает все транзитные очереди, их waiters и route timers даже при остающемся живым ETCP. Повторный destroy допустим.
tx_stats не создаёт канал: 0 — снимок, 1 — отсутствует/закрыт с нулевым out, -1 — аргументы/время. queued/inflight/acked bytes измеряют исходный payload. Повторы не увеличивают acked-итог. ready означает подтверждённый peer_reset_id, не READY группы и не гарантированный физический UP. RTT хранится в 0.1 ms, в snapshot переводится в микросекунды. Jitter — беззнаковый 16.16 EWMA |ΔRTT|; монитор публикует удвоенный EWMA без сужения до uint16. История delivery и параметры RTT/probe сбрасываются при смене логической сессии.
Проверки и диагностика
- test_etcp_router_unit: 43 проверки seq/reorder, crypto policy/verify/decrypt, RECEIVE_BUSY, метрик, nested close/reopen, вложенной self-доставки, предельной длины, unsigned jitter и очистки pending transit. Fixture освобождает собственные registry-ссылки и проверяет отсутствие оставшихся u_malloc-объектов.
- test_etcp_router_faults: 15 сценариев потерь, перестановок, дубликатов, duplex/multiplexing, handshake, старых эпох/ACK, wraparound, смены маршрута, закрытия из callbacks, timeout и десятисекундного разрыва звонка.
- test_bgp_paths: реальные BGP/роутеры в управляемой сети четырёх узлов, все 64 графа, signed+encrypted transit, петли DATA/ACK/control, повреждённые пути, смена занятого next hop и cleanup.
- test_etcp_router, test_etcp_router_tcp, test_etcp_router_reconnect: UDP/STCP, backpressure, reset транспорта и транзит с перезапуском промежуточного узла.
ROUTER_TEST_LOG=/tmp/router.log включает подробный файл в fault/network-наборах. Категория etcp_route: DEBUG — эпохи/повторы/состояния, TRACE — отдельные DATA/ACK. Ожидаемые WARN/ERROR отрицательных сценариев не скрываются.