Files
utun2/src/routing_layer/etcp_router_doc.md
T

19 KiB
Raw Blame History

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 отрицательных сценариев не скрываются.