Reconcile router protocol and architecture with implementation

This commit is contained in:
evgeny committed 2026-10-24 15:15:35 +02:00
1 parent 09e620e12a
commit 4d9e624f8e
2 files changed
+289 -398

No files matched your search

+125 -305
View File
@@ -1,329 +1,149 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
# Архитектура ETCP Router
# ETCP Router Architecture
Маршрутизатор связывает сервис с удалённым узлом через группу, не привязывая логическую
сессию к одному физическому ETCP. Ключ канала — `(group_id, remote_node_id, svc_id)`.
Wire-формат и сервисный API подробно описаны в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md),
объявления — в [etcp_router.h](../src/routing_layer/etcp_router.h).
## Структуры данных
## Отправка и приём
```
ETCP_ROUTER_CONN (один на пару remote_node_id + svc_id)
┌─────────────────────────────────────────────────────┐
│ ll_entry (хеш-индекс: remote_node_id[8] + svc_id[1]) │
│ remote_node_id: u64 │
│ svc_id: u8 │
│ last_dgram_ts: u16 (timestamp последней датаграммы) │
│ │
│ tx_seq: u32 следующий seq для отправки │
│ rx_seq: u32 ожидаемый seq для сборки │
│ tx_acked: u32 сколько наших пакетов подтвердил remote │
│ last_sent_ack_seq: u32 последний отправленный ACK │
│ │
│ recv_q: ll_queue* очередь reorder (хеш по seq[4]) │
│ send_q: ll_queue* очередь при переполнении inflight │
│ │
│ ack_timer: void* периодический 100ms │
│ idle_ack_timer: void* idle таймаут 500ms │
│ send_waiter: queue_waiter_handle waiter на send_input_q │
│ watchdog_timer: void* проверка инварианта 500ms │
│ send_blocked: u8 inflight полон │
└─────────────────────────────────────────────────────┘
SVC_ROUTE_HDR (22 байта)
┌──────┬────────────────┬────────────────┬───────┬────────┬──────────┐
│ cmd │ dst_node_id │ src_node_id │ seq │ svc_id │ payload │
│ u8 │ u64 │ u64 │ u32 │ u8 │ ... │
└──────┴────────────────┴────────────────┴───────┴────────┴──────────┘
seq в data-пакетах = tx_seq (счетчик отправленных)
seq в ACK-пакетах = rx_seq (ожидаемый следующий)
CLOSE-флаг = 0x80000000 в seq, RST-флаг = 0x40000000
```mermaid
flowchart TD
Producer["Сервис: payload + mode"] --> Send["send_q: plaintext, seq, mode"]
Send --> Session{"peer_reset_id подтверждён?"}
Session -- нет --> Hello["HELLO / challenge / confirm"]
Hello --> Session
Session -- да --> Route["Текущий маршрут: conn_mgr → BGP, либо прямой глобальный ETCP"]
Route --> Wait["Waiter физической send_input_q"]
Wait --> Encode["route_crypto_encode: encrypt → sign"]
Encode --> Inflight["inflight_q: неизменяемое тело без visited-list"]
Inflight --> Path["Добавить source ID"]
Path --> ETCP["etcp_send / normalizer"]
Inflight -- RTO 300 ms --> Wait
ETCP --> Network["Сеть / транзитные узлы"]
Network --> Check["Проверить visited-list"]
Check --> Destination{"dst=self?"}
Destination -- нет --> Transit["Добавить self один раз, BGP next hop / transit FIFO"]
Transit --> Network
Destination -- да --> Decode["Crypto policy + verify/decrypt + проверка эпох"]
Decode --> Incoming["incoming_q: plaintext"]
Incoming --> Reorder["recv_q: reorder по seq"]
Reorder --> Ready{"receive_ready разрешает?"}
Ready -- нет --> Busy["Сохранить пакет, ACK RECEIVE_BUSY"]
Busy --> Resume["resume_receive: отложенное продолжение"]
Resume --> Reorder
Ready -- да --> Callback["Callback сервиса получает entry"]
Callback --> Ack["rx_seq++ → ACK текущей границы"]
Ack --> Release["Отправитель снимает подтверждённый inflight"]
```
## Send Path (отправка данных)
Каждая кодограмма имеет самостоятельный seq; это не байтовый поток.
send_q ограничена 64 пакетами при обычной отправке, `force` обходит этот порог.
Inflight по умолчанию — 256, при minRTT probe — 4. Получатель ограничивает reorder
окном max_inflight и суммарным размером очередей max_inflight+1 записей.
```
etcp_route_send(inst, dst, entry)
etcp_router_conn_send(rconn, data, len)
│
▼
┌─────────────────────┐
│ rconn = find/get │ поиск по (dst, svc_id)
│ авто-создание если │ или etcp_router_conn_get()
│ ещё нет │
└────────┬────────────┘
│
▼
┌──────────────────────────────┐
│ router_enqueue_send(rconn) │ всегда через send_q (FIFO)
│ if send_q >= 64 && !force: │ → вернуть -1 (backpressure)
│ return -1 │
└──────┬───────────────────────┘
│ копия в send_q + router_send_kick()
▼
┌─────────────────────────────────────────┐
│ router_send_kick(rconn) │
│ closed/no_route → return │
│ send_q пуст → return │
│ inflight >= limit → send_blocked=1, ret │
│ waiter уже ждёт → return │
│ нет conn → set_no_route │
│ queue_waiter_wait(send_input_q, │
│ &send_waiter, drain_cb) │
└──────┬───────────────────────────────────┘
│ send_input_q опустел (round-robin: FIFO waiter-ов)
▼
┌──────────────────────────────┐
│ router_send_drain_cb(rconn) │ шлём ОДИН пакет
│ e = send_q.pop() │
│ router_send_one_flags(e) │
│ → tx_seq++ │
│ → etcp_send(conn, entry) │
│ если send_q ещё непуст: │
│ inflight полон → blocked=1│
│ иначе → queue_waiter_wait │ (в хвост → round-robin)
│ если пуст → send_blocked=0 │
└──────────────────────────────┘
Все события выполняются в uasync-потоке. Штатный normalizer откладывает physical waiters,
а callbacks производителей send_q могут выполняться синхронно при освобождении места.
Поэтому код отправки повторно проверяет состояние/эпохи после callback.
Повторный запуск drain:
• приход ACK → router_handle_ack → kick
• маршрут появился → router_no_route_retry_cb → kick
• рост inflight-лимита → router_update_inflight_limit → kick
• страховка → router_send_watchdog_cb (500ms) → DEBUG_ERROR + kick
## Эпохи логического канала
Полный сетевой пакет:
`[57-byte SVC_ROUTE_HDR][payload][signature?][visited_ids:8*N][count:1]`
Числа в заголовке — native endian. flags — отдельный байт; биты seq не заняты флагами.
Заголовок включает group/src/dst/service, seq/timestamp и поля reset_id/peer_reset_id/challenge.
visited-list содержит 1..64 ID, находится вне подписи/шифрования и проверяется на каждом переходе.
Первичное согласование двух сторон имеет вид:
```mermaid
sequenceDiagram
participant A as Узел A, local=a
participant B as Узел B, local=b
A->>B: HELLO(reset=a, peer=0)
B->>A: CHALLENGE(reset=b, peer=a, cB)
A->>B: CONFIRM(reset=a, peer=b, cB)
A->>B: CHALLENGE(reset=a, peer=b, cA)
B->>A: CONFIRM(reset=b, peer=a, cA)
Note over A,B: Каждый узел принимает peer ID только после ответа на свой pending challenge
```
## Receive Path (приём данных)
Получение HELLO/CHALLENGE с неизвестной эпохой само по себе не сбрасывает работающий канал.
Переставленные/старые CONFIRM не совпадают с pending ID/challenge и игнорируются.
Новые challenge ограничены интервалом 300 ms; первичный HELLO повторяется без отдельного timeout.
```
etcp_router_recv_cb(conn, entry)
│
▼
┌──────────────────────┐
│ dst == node_id? │
└──┬───────────────┬───┘
│ YES │ NO (транзит)
▼ ▼
┌────────────────────┐ ┌──────────────────────────┐
│ Мы — целевая нода │ │ Транзит: найти next hop │
└────────┬───────────┘ │ route_bgp_find_conn_for_ │
│ │ node(bgp, hdr.dst) │
▼ │ etcp_send(next, entry) │
┌────────────────────┐ └──────────────────────────┘
│ pl_len == 0? │
└──┬───────────┬─────┘
│ YES │ NO (данные)
▼ ▼
┌─────────┐ ┌─────────────────────────┐
│ CLOSE/ │ │ rconn = find/get │
│ RST? │ │ (авто-создание если нет) │
└──┬──┬───┘ └───────────┬─────────────┘
│ │ │
│ │ NO (ACK) ▼
│ ▼ ┌───────────────────────────────────────┐
│ ┌──────────┐│ Проверка границ seq: │
│ │ tx_acked ││ d = seq - rx_seq │
│ │ = hdr.seq││ |d| < ROUTER_MAX_INFLIGHT? │
│ │ (только ││ │
│ │ вперёд) ││ Дубликат? │
│ │ kick ││ rx_seq - seq > 0 ИЛИ │
│ └──────────┘│ seq уже в recv_q? │
│ └──────┬─────────────┬──────────────────┘
│ │ OK │ out-of-bounds / dup
│ ▼ ▼
│ ┌──────────────┐ ┌─────────┐
│ │ recv_q.put() │ │ DROP │
│ │ хеш по seq │ └─────────┘
│ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────────────┐
│ │ seq == rx_seq? │
│ └──┬───────────────┬───┘
│ │ YES │ NO
│ ▼ │
│ ┌──────────────┐ │
│ │ try_assembly │ │
│ │ while next в │ │
│ │ recv_q: │ │
│ │ deliver() │ │
│ │ rx_seq++ │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┴──┐
│ │ router_deliver() │
│ │ вызывает callback │
│ │ сервиса (svc_id) │
│ │ entry: [svc_id|payload] │
│ └──────────────┬──────────┘
│ │
▼ ▼
┌────────────────────────────────────┐
│ router_schedule_ack(rconn) │
│ запускает ack_timer (100ms) если │
│ ещё не запущен │
└────────────────────────────────────┘
```
| Событие | Что сохраняется | Что меняется |
| --- | --- | --- |
| Смена ETCP/next hop | Логические эпохи, seq, очереди | Физический waiter/маршрут |
| Первое подтверждение peer | Накопленная send_q | Заполняется peer_reset_id, начинается отправка |
| Подтверждённый рестарт peer | Собственный reset_id | Очереди, seq, RTT/метрики; уведомление сервиса |
| Явный conn_restart | Ключ group/peer/service и адрес rconn | Новый reset_id, пустое состояние; затем callback и HELLO |
| conn_close | Только данные, переданные во владение callback | Запись снимается с реестра, ресурсы освобождаются |
```
CLOSE / RST ветка:
┌────────────────────────────────────────┐
│ pl_len == 0 && seq & CLOSE/RST │
│ → router_close_and_notify(rconn) │
│ ├─ router_send_close_to_service() │
│ │ (callback с NULL conn) │
│ ├─ отправить CLOSE удалённой стороне│
│ ├─ отмена всех таймеров │
│ ├─ очистка recv_q + send_q │
│ └─ удаление из inst->router_conns │
└────────────────────────────────────────┘
```
Сервис возобновляет прикладную синхронизацию после сброса; старые гарантии доставки через reset не продолжаются.
Готовность router означает наличие подтверждённого peer ID. Это отдельное состояние
от физического UP и READY BGP-группы.
## ACK Mechanism
## Подтверждение и пауза потребителя
```
┌─────────────────────────────────────────┐
│ router_schedule_ack(rconn) │
│ (вызывается при получении данных) │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ если idle_ack_timer активен — отменить │
│ если ack_timer не активен — запустить │
│ ROUTER_ACK_INTERVAL_TB = 1000 (100ms) │
└────────────────┬────────────────────────┘
│
▼ (через 100ms)
┌─────────────────────────────────────────┐
│ router_ack_timer_cb() │
│ │
│ if rx_seq != last_sent_ack_seq: │
│ router_send_ack(rconn) │
│ last_sent_ack_seq = rx_seq │
│ перезапустить ack_timer │
│ else: │
│ запустить idle_ack_timer (500ms) │
└────────────────┬────────────────────────┘
│
▼ (через 500ms)
┌─────────────────────────────────────────┐
│ router_idle_ack_timer_cb() │
│ │
│ if rx_seq != last_sent_ack_seq: │
│ router_send_ack(rconn) (досылка) │
│ last_sent_ack_seq = rx_seq │
└─────────────────────────────────────────┘
DATA проходят crypto policy и verify/decrypt до изменения reorder/rx_seq.
Сервис получает `[svc:1][src:8][dst:8][rx_flags:1][group:8][payload]`, всего 26 байт заголовка.
Его callback владеет entry, ACK продвигается после успешной передачи callback.
router_send_ack(rconn):
hdr.seq = rx_seq
hdr.svc_id = svc_id
pl_len = 0 → это ACK-пакет
etcp_send(conn, entry)
```
ACK подтверждает только диапазон до tx_sent, в модульном порядке uint32.
При продвижении снимаются существующие inflight-записи и один раз учитываются исходные payload bytes.
```
Обработка входящего ACK:
┌────────────────────────────────────────┐
│ pl_len == 0 && нет CLOSE/RST флагов │
│ │
│ if seq >= tx_acked: // только вперёд │
│ tx_acked = seq // обновить │
│ else: │
│ DEBUG_WARN "stale ACK" │
│ │
│ if send_blocked: │
│ send_blocked = 0 │
│ router_send_kick(rconn) │
└────────────────────────────────────────┘
```
RECEIVE_BUSY не подтверждает отложенные DATA, но поддерживает живость ожидающего диска/буфера сервиса.
Даже при прежнем seq допустимый BUSY обновляет отсчёт RTO и останавливает новую отправку/проход ретрансмитов.
Обычный повторный ACK без продвижения этого не делает. Снятие BUSY возобновляет send_q.
## Inflight Control
receive_ready — предикат без I/O и изменения/закрытия роутера. При 0 пакет сохраняется в recv_q;
resume_receive планирует продолжение. Управляющий CLOSE не проходит через этот предикат.
```
ROUTER_MAX_INFLIGHT = 256
| Механизм | Интервал/условие |
| --- | --- |
| ACK | Не чаще 10 ms; текущая граница повторяется при дубликате DATA после потери ACK |
| RECEIVE_BUSY | Около 100 ms, пока сервис остановлен |
| Idle ACK | 500 ms, только если rx_seq ещё не был отправлен в ACK |
| Handshake/RTO | 300 ms |
| Нет маршрута после handshake | Повторный поиск через 20 ms |
| Транзит при backpressure | Waiter + повтор выбора маршрута через 20 ms |
| Watchdog send_q | 500 ms, пока очередь непуста |
| Нет прогресса/BUSY | Закрытие после 17 циклов, для сервиса CALL — после 67 |
Отправка возможна когда: tx_seq - tx_acked < 256
Отправка заблокирована: tx_seq - tx_acked >= 256
Транзитные очереди принадлежат физическому ETCP, сгруппированы по group/src/dst и не имеют своих seq.
При смене маршрута пакет переносится на актуальный next hop без ожидания освобождения старой очереди.
Отсутствие пути приводит к потере транзитной копии; её восстанавливает end-to-end inflight источника.
При блокировке:
данные → send_q (FIFO, без дропов)
send_blocked = 1
## Владение и локальная доставка
Разблокировка:
приход ACK от remote → tx_acked обновляется
→ router_send_kick() → waiter → drain_cb разгребает send_q
или маршрут появился / inflight-лимит вырос → kick
страховка: router_send_watchdog_cb (500ms) → force kick
| Объект | Владение |
| --- | --- |
| Входной entry etcp_route_send | Всегда поглощается, включая ошибку |
| Буфер etcp_router_conn_send | Заимствован на время копирования |
| ETCP_ROUTER_CONN из conn_get | Заимствован до close/destroy |
| Сервисный entry | Передаётся callback; освободить dgram и entry |
| Физический conn в callback | Заимствован, может обозначать транзитного пира |
| Синтетический conn | Только на время callback, instance/peer=self; без сетевого send/ref_take |
| Send-ready handle/arg | У производителя до callback/отмены; перед освобождением отменить |
send_q полностью разобран → send_blocked = 0
```
Self-доставка через etcp_route_send синхронна и не создаёт канал. Она обходит сетевые crypto policy
и receive_ready, выдаёт rx_flags=0; режим проверяется на допустимые биты.
Payload должен быть непустым и помещаться вместе с 26-байтным заголовком в uint16 длину.
Нет обработчика, переполнение размера или отказ памяти возвращают -1.
Отдельный синтетический conn каждого вызова сохраняет контекст при вложенных доставках разных ядер.
## Полная схема потоков
Закрываемый канал удаляется из реестра до callback. Callback может закрывать группу/другие каналы
или открыть замену с тем же ключом; старая запись не возвращается повторным поиском.
Память канала и send_q освобождается отложенно за пределами текущего callback.
До массовой остановки сервисы отменяют producers/waiters; уничтожение ядра из callback останавливаемого сервиса запрещено.
```
┌──────────┐
│ Сервис │ (routing, proxy, NAT, ...)
│ (svc_id)│
└────┬─────┘
│
etcp_route_send() │ callback(svc_id, entry)
▼ ▲
┌─────────┐ send ┌─────────────────────────────────┐ recv ┌─────────┐
│ TUN │─────────▶│ ETCP ROUTER │─────────▶│ TUN │
│ (выход) │ │ │ │ (вход) │
└─────────┘ │ ┌──────────┐ ┌──────────┐ │ └─────────┘
│ │ send_q │ │ recv_q │ │
│ │ (FIFO) │ │ (хеш seq)│ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────┐ │
│ │ Inflight / Assembly │ │
│ │ tx_seq - tx_acked │ │
│ │ rx_seq → deliver │ │
│ └───────────┬─────────────┘ │
│ │ │
└──────────────┼──────────────────┘
│
┌──────▼──────┐
│ BGP │
│ (нахождение│
│ next hop) │
└──────┬──────┘
│
┌──────▼──────┐
│ ETCP conn │──▶ сеть
└─────────────┘
close_group удаляет логические каналы и pending transit указанной группы.
destroy снимает wire callback, закрывает все логические каналы и очищает транзитные очереди,
таймеры и waiters на всех живых ETCP. Уничтожать сам ETCP для очистки роутера не требуется.
──▶ send (исходящие данные)
──▶ recv (входящие данные, callback сервису)
──▶ транзит (через BGP к следующему hop)
```
## Таймеры (один rconn)
```
┌─────────────────────────────────────────────────────┐
│ │
│ ack_timer ──────── 100ms ─────▶ router_ack_timer_cb│
│ (периодический) │
│ │
│ idle_ack_timer ─── 500ms ─────▶ router_idle_ack_cb │
│ (досылка последнего ACK) │
│ │
│ watchdog_timer ── 500ms ─────▶ router_send_watchdog│
│ (страховка инварианта drain, force kick) │
│ │
└─────────────────────────────────────────────────────┘
```
## Флаги seq
```
┌──────────────────────────────────────────────────┐
│ CLOSE: 0x80000000 нормальное закрытие conn │
│ RST: 0x40000000 conn не найден (reset) │
│ │
│ CLOSE/RST-пакеты: seq=0 | флаг, pl_len=0 │
│ Data-пакеты: seq=tx_seq, pl_len>0 │
│ ACK-пакеты: seq=rx_seq, pl_len=0 │
└──────────────────────────────────────────────────┘
```
Метрики delivery считают первые подтверждения исходного payload, без заголовков/crypto/ретрансмитов.
RTT — 0.1 ms, snapshot API выдаёт микросекунды; jitter — unsigned 16.16 EWMA,
монитор публикует удвоенный EWMA. Очереди, метрики и параметры minRTT сбрасываются вместе с логической сессией.
+164 -93
View File
@@ -1,121 +1,192 @@
# ETCP Router: протокол и проверка транспорта
# ETCP Router: протокол и сервисный API
Логическое соединение определяется `(group_id, remote_node_id, svc_id)`. Роутер обеспечивает порядок, дедупликацию и повторную отправку между конечными узлами; физический ETCP и транзитные узлы могут меняться. Формат изменён без обратной совместимости.
Логический канал определяется тройкой `(group_id, remote_node_id, svc_id)`.
Роутер обеспечивает порядок, дедупликацию и повторную отправку между конечными узлами;
физический ETCP и транзитные узлы могут меняться. Работа и callbacks выполняются в uasync-потоке ядра.
Объявления и точные константы — в `etcp_router.h`, схема потоков — в
[архитектурном описании](../../doc/etcp_router_arch.md). Формат не имеет согласования версий;
узлы используют один формат сборки, обратная совместимость не поддерживается.
## Wire-формат
Packed `SVC_ROUTE_HDR`, 57 байт:
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`
Числовое представление полей сохраняет существующий формат проекта (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; на конечном узле полный список допустим, для следующего перехода места уже нет.
Источник начинает список собственным ID. Каждый транзитный узел проверяет список и добавляет
свой ID один раз на приёме, перед ожиданием очереди. При повторной отправке источника список создаётся заново.
Проверяются границы, 1..64 ID, отсутствие нулей/дубликатов/собственного ID, первый ID=src,
последний ID=ETCP peer. Полный список из 64 ID допустим на конечном узле; следующий транзит уже невозможен.
Список находится вне end-to-end подписи/шифрования и снимается перед crypto decode.
Это защита от временных петель при сходимости BGP, а не криптографическое подтверждение пути.
Это защита от временных петель BGP, а не доказательство пути от недоверенного пира.
Транзит не декодирует payload. При занятой физической очереди он сохраняет пакет в FIFO
для `(group, src, dst)`; waiter и таймер 20 ms повторно выбирают путь.
При исчезновении маршрута пакет отбрасывается, повтор доставки выполняет источник.
Маршруты строит BGP по правилам `topo_group.h`. Транзит использует текущий next hop.
При backpressure пакет ждёт в очереди; waiter и таймер 20 ms повторно проверяют маршрут.
Смена пути не требует освобождения старой физической очереди. ID добавляется один раз на приёме,
а не при каждой попытке выгрузки. При исчезновении маршрута транзит отбрасывается; доставку повторяет источник.
Отправитель сначала выбирает транспорт через conn_mgr, затем BGP группы; при отсутствии группы
допускается прямой глобальный транспорт. Транзит требует маршрута зарегистрированной группы:
одно прямое соединение не разрешает ретрансляцию для неизвестного group_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 | нет | оба ID, эхо challenge |
| DATA | 0 либо SIGNED/ENCRYPTED | непустой | seq, оба ID, challenge=0 |
| ACK | 0 | нет | seq=следующий ожидаемый, оба ID, challenge=0 |
| CLOSE | CLOSE (0x02) | нет | оба ID, challenge=0 |
`reset_id` — случайный ненулевой ID локального экземпляра канала,
`peer_reset_id` — подтверждённый ID получателя. DATA/ACK/CLOSE допускаются только для текущей пары.
Смена физического транспорта пару не меняет.
Незнакомый ID сам по себе не сбрасывает рабочую сессию. Получатель выдаёт свежий challenge и меняет peer ID только после соответствующего CONFIRM. CHALLENGE не меняет рабочую сессию: отправляется CONFIRM и, если нужно, встречный challenge. Потерянные сообщения handshake восстанавливаются повторением HELLO через 300 ms; выдача новых challenge ограничена тем же интервалом.
| Исходящий пакет | 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 |
Первичное согласование сохраняет накопленные исходящие данные. Подтверждённый рестарт пира сбрасывает seq/очереди и уведомляет сервис; собственный ID сохраняется. Явный `etcp_router_conn_restart` создаёт новый собственный ID. Сброс выполняется до уведомления, поэтому данные, отправленные сервисом из callback, сохраняются. Сервис должен заново выполнить прикладную синхронизацию после уведомления; гарантии доставки не продолжаются через явный сброс сессии.
Незнакомый ID не сбрасывает рабочую сессию: получатель сначала выдаёт challenge.
CONFIRM должен совпасть с локальным ID, pending peer ID и challenge.
CHALLENGE вызывает ответ CONFIRM и при необходимости встречную проверку, но сам по себе не меняет сессию.
Выдача новых challenge ограничена интервалом 300 ms; пока первичный peer не подтверждён,
HELLO повторяется каждые 300 ms без отдельного таймаута. При этом исходные данные остаются в send_q.
На этом этапе отсутствие маршрута обрабатывается тем же HELLO-таймером.
Challenge защищает от случайных старых/переставленных управляющих пакетов, но не является end-to-end аутентификацией против злонамеренного транзитного узла. Управление опирается на защиту физических ETCP-соединений. Подпись/шифрование DATA включаются вызывающим сервисом через `ROUTE_CRYPTO_SIGN` / `ROUTE_CRYPTO_ENCRYPT`.
Первичное согласование сохраняет накопленные исходящие данные.
Подтверждённый рестарт пира очищает seq/очереди/метрики и уведомляет сервис, сохраняя собственный ID.
Явный `etcp_router_conn_restart` выбирает новый собственный ID. Отсутствующий канал не создаётся.
Сброс выполняется до уведомления, поэтому отправка из callback относится уже к новой сессии.
Гарантии прежнего канала не продолжаются через его сброс; прикладную синхронизацию возобновляет сервис.
## Очереди и подтверждения
Challenge защищает от старых/переставленных пакетов, но не аутентифицирует end-to-end управление
против злонамеренного транзитного узла. Управление опирается на защиту физических ETCP-соединений.
DATA кодируются через route_crypto: сначала шифрование, затем подпись заголовка и payload.
- `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 без прогресса ACK планируется проход повторной отправки. Логический канал закрывается после 67 циклов без прогресса для `ETCP_RT_ID_CALL` (около 20 s), для остальных сервисов — после 17 циклов (около 5 s). Отсутствующий маршрут проверяется каждые 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 запрещён: он используется для управления.
## Очереди и seq
## Сервисный API
| Состояние | Содержимое/смысл |
| --- | --- |
| 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 |
`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` обходит порог очереди и не предназначен для непрерывного потока.
Начальные seq равны 0. Сравнения выполняются по модульной разности uint32.
Обычная отправка ограничена 64 пакетами send_q; `force` обходит этот порог, но не проверку размера/режима.
Кодирование выполняется при реальной отправке после handshake, с текущими эпохами.
После успешного etcp_send запись снимается с send_q; при отказе исходный пакет остаётся в очереди.
`etcp_router_bind` регистрирует callback, владеющий переданным ll_entry. Формат доставки — 26 байт заголовка:
Обычный 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...`
Читать невыравненные числовые поля нужно через `memcpy`. Заголовок без payload означает уведомление о закрытии/сбросе. После обработки освобождаются `queue_dgram_free(entry)` и `queue_entry_free(entry)`.
Заголовок доставки — 26 байт. Числовые поля не выровнены, читать через memcpy.
rx_flags содержит crypto-флаги исходного сетевого пакета. Пустой payload — уведомление close/reset.
После обработки сервис освобождает queue_dgram_free(entry) и queue_entry_free(entry).
Callback может закрыть соединение. Освобождение send_q отложено за пределы callback производителя; отправка проверяет, не закрылся/перезапустился ли rconn. При уничтожении реестр удаляется ровно один раз для каждого соединения.
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_bgp_paths` использует настоящий BGP и роутер в управляемой сети четырёх узлов:
все 64 графа, разбиение/слияние, изменения при непустых очередях, REINIT, выбор кратчайшего пути
с устойчивым разрешением равенства. Дополнительно проверяет подписанный зашифрованный транзит,
петли DATA/ACK/control, повреждённые списки переходов, смену заблокированного next hop и очистку таймеров.
Счётчики `UTUN_INSTANCE.router_loop_drops` и `router_path_errors` сопровождаются WARN с идентичностью пакета.
- 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 транспорта и транзит с перезапуском промежуточного узла.
`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 таймеров, завершился без диагностических ошибок.
ROUTER_TEST_LOG=/tmp/router.log включает подробный файл в fault/network-наборах.
Категория etcp_route: DEBUG — эпохи/повторы/состояния, TRACE — отдельные DATA/ACK.
Ожидаемые WARN/ERROR отрицательных сценариев не скрываются.