Reconcile router protocol and architecture with implementation
This commit is contained in:
1 parent
09e620e12a
commit
4d9e624f8e
2 files changed
+289
-398
No files matched your search
+125
-305
@@ -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 сбрасываются вместе с логической сессией.
|
||||
@@ -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 отрицательных сценариев не скрываются.
|
||||
Reference in new issue
Block a user