You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
24 KiB
226 lines
24 KiB
Формат кодограммы: |
|
------------------ |
|
пакеты идут по следующему пути: |
|
пакет кем-то помещается в входную очередь (ETCP input_queue). |
|
Когда load balancer готов отправить следующий пакет он делает вызов etcp_request_pkt в etcp.c/h и etcp либо формирует следующую кодограмму или встаёт в состояние async ожидания (возвращая null): |
|
- либо wait ETCP input_queue (если нечего передавать) |
|
- либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия". |
|
из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку. |
|
как формируется пакет: |
|
1. выбираем канал передачи (etcp_loadbalancer_select_link) |
|
2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем |
|
3. если пакет найден или есть неподтверждённые ACK: |
|
1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало |
|
2. в конец добавляем секцию 0x00 с данными пакета |
|
при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен |
|
Функция прикрепления опциональных секций: |
|
- запрашивает выбор канала передачи. |
|
- добавляет накопившиеся ACK |
|
При приёме пакета (от etcp_connections): |
|
- последовательно сканируем секции и отдаём их на обработку нужным обработчикам: |
|
- ack (0x01): помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также снимаем блокировку отправки |
|
- timestamp (0x06): обновляем last RTT, пересчитываем RTT avg10 и jitter по каждому линку; обновляем recv_dt_avg для раздельного измерения времени передачи/приёма |
|
- [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). также добавляем ACK запись в ack_q для отправки. |
|
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть. |
|
|
|
пересканирование inflight: |
|
- запускается при добавлении пакета в очередь ожидания ACK (wait_ack_cb). устанавливается таймер на первый неистекший пакет. |
|
- если время последней отправки > RTT_avg10*K1+jitter*K2 (коэффициенты K1=32, K2=32, деление на 16) то пакет заново отправляется |
|
- очередь FIFO (сортирована по timestamp добавления = времени отправки), сканирование прекращается на первом неистекшем пакете |
|
- по истечению таймера (ack_timeout_cb) снова вызывается проверка очереди |
|
|
|
inflight - две очереди (ll_queue): |
|
- input_send_q: список пакетов ожидающих отправку |
|
- input_wait_ack: список неподтвержденных пакетов ожидающих ack |
|
При отправке пакет перемещается из input_send_q в input_wait_ack. |
|
При ретрансмиссии пакет перемещается из input_wait_ack обратно в input_send_q. |
|
доп. свойства пакетов (struct INFLIGHT_PACKET): |
|
- указатель на ETC_LINK последней отправки (last_link) |
|
- timestamp последней отправки (last_timestamp) |
|
- число отправок (send_count) |
|
- send_hist[8] - история через какие линки отправлялся (индекс линка) |
|
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK |
|
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight. |
|
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся |
|
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384). |
|
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу: |
|
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет |
|
input_queue даже если подтверждённые записи уже удалены по selective ACK. |
|
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap. |
|
optimal_inflight расчитывается из RTT и bandwidth линка. |
|
|
|
bandwidth по каждому линку адаптивно подстраиваем: |
|
Периодически в stats_timer_cb собираем статистику окна (rtt_avg10, ретрансмиссии, переданные пакеты). |
|
BW рассчитывается как: inflight_lim_bytes * 8 / rtt_min (в секундах) * 8 / 1024 * 1.3 (Kbits/sec). |
|
Размер inflight_lim_bytes плавно подстраивается вверх/вниз в зависимости от того, растёт ли RTT. |
|
Burst-измерения bandwidth (0x07, 0x08) зарезервированы, но на данный момент не реализованы. |
|
|
|
|
|
**** Формат кодограмм для etcp.c/h **** |
|
Каждая кодограмма состоит из обязательного заголовка и опциональных секций: |
|
1. Обязательный заголовок всего пакета (3 байта) добавляется при передаче, есть во всех пакетах: |
|
[Timestamp high][Timestamp low][flags/flag_up] |
|
- Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется |
|
- flag_up: uint8_t, bit0 = up/down (recv_keepalive) — признак живой приёмной стороны |
|
|
|
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах). |
|
timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке). |
|
Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных. |
|
2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка): |
|
а) Подтверждения (ACK) - заголовок 0x01: |
|
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count] |
|
- count: количество ACK записей (1 байт; до 32 ограничено размером пакета) |
|
- last_delivered_id: uint32_t — последний ID, доставленный получателю (по этот ID всё собрано подряд) |
|
- rx_dup_count: uint16_t — счётчик принятых дубликатов (локальный, для вычисления tx_dup_count на удалённой стороне) |
|
- seq: uint32_t — номер подтверждаемого пакета |
|
- recv_ts: uint16_t — локальный timestamp приёма пакета (в единицах 0.1 мс) |
|
- dly: uint16_t — задержка между приёмом пакета и отправкой ACK (для расчёта RTT) |
|
Каждая запись = 8 байт. |
|
в) channel timestamp (добавляется после выбора канала передачи, если есть свежие данные): |
|
[0x06] [RET_Timestamp(2 байта LE)] [RECV_Rel_Timestamp(2 байта LE)] |
|
- RET_Timestamp: uint16_t = last_recv_timestamp + (now - last_recv_local_time) |
|
(оценка текущего времени на передающей стороне в момент отправки) |
|
- RECV_Rel_Timestamp: uint16_t = last_recv_local_time - last_recv_timestamp |
|
(относительное смещение между локальным и удалённым временем) |
|
Всего 5 байт. Добавляется только если last_recv_updated=1 (был принят пакет с момента последней отправки) |
|
и если прошло не более ~100ms (dt < 1000000 таймбаз). |
|
3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя): |
|
[0x00] [ID 4 байта LE] [данные...] |
|
- ID: uint32_t, 32-битный циклический номер пакета |
|
Пакет может содержать несколько опциональных секций (например, ACK + channel_timestamp + данные), |
|
всегда в порядке: ACK, channel_timestamp, payload; payload всегда последний. |
|
Разбор секций — последовательное сканирование с известными размерами (нет явного поля длины секции). |
|
|
|
цель протокола: |
|
1. контролировать состояние очередей (не накапливать данные где этого можно избежать), анализировать RTT и inflight - динамически подстраивать размер окна и ретрансмиссии, утилизируя сеть насколько это возможно но не допуская наполнения буферов в сети (что приводит к бестолковому увеличению RTT) |
|
|
|
Установка подключения для канала (реализуется в etcp_connections.c/h): |
|
каждое ETCP подключение может содержать несколько линков до endpoint (разные маршруты, каналы связи). |
|
При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно): |
|
- RTT последнего пакета и время отправки последнего пакета |
|
- общее количество отправленных/принятых байт |
|
- количество перезапросов (по каждому каналу отдельно) в пакетах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики. |
|
- обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент |
|
Планировщик/балансир каналов (etcp_loadbalancer.c/h): |
|
для каждого канала ограничение полосы с тестированием отклика: |
|
- полосу ограничиваем по объему данных - полному числу байт ethernet пакта (добавляем размер заголовков) |
|
Ограничение реализуем следующим образом: |
|
- в структуре есть счётчик времени (квантизация стандартная 0.1ms) по которое канал загружен данными. И суб-разряды (nanotime) этого счетчика 0.1ns - 0.1ms (считает по модулю 1000000 и переполнения добавляет в основной счетчик) чтобы при высоких скоростях не было потери точности. |
|
Этим счётчиком контролируется полоса: при передаче к нему добавляется расчётное время передачи текущего ethernet пакета (используя текущий bandwidth считаем время передачи одного байта (float) и умножаем на число байт, переводим в таймбазу nanotime). А если при передаче время меньше current_time - delta_time (delta_time - константа в define, x0.1ms) то он устанавливается как current time-delta_time. (при неактивности обновляем время) |
|
счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи"); |
|
когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения. |
|
также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client. |
|
процесс установки соединения (рукопожатие): |
|
- клиент отправляет init запрос и выставляет таймаут |
|
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init. |
|
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive. |
|
- сервер при получении init запроса: если линк существует и session_id совпал — отвечает без сброса (0x05); если session_id новый — выполняет etcp_conn_reinit и отвечает со сбросом (0x03). |
|
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT со сбросом (0x02); если link_state=2 (reconnect) — без сброса (0x04). |
|
link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected. |
|
|
|
**** Формат кодограмм для etcp_connections.c/h **** |
|
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) ID не используется. |
|
|
|
Типы пакетов: |
|
0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии) |
|
0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом) |
|
0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса) |
|
0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса) |
|
0x06 — ETCP_PING (проверка доступности) |
|
0x07 — ETCP_PONG (ответ на пинг) |
|
|
|
1) Init запрос (0x02 / 0x04): |
|
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [keepalive_interval(2)] |
|
[recovery_interval/100(2)] [local_link_id(1)] [sock_id(1)] [only_local(1)] [type(1)] |
|
[random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)] |
|
- code: 0x02 (со сбросом) или 0x04 (без сброса) |
|
- node_id: 64 бита, идентификатор узла (big-endian) |
|
- session_id: 32 бита, случайный ID сессии генерируется клиентом (big-endian) |
|
- mtu_local: uint16_t локальный MTU |
|
- keepalive_interval: uint16_t интервал keepalive в мс |
|
- recovery_interval: uint16_t = recovery_interval/100 (в таймбазах 0.1мс / 100) |
|
- local_link_id: uint8_t (0-255), назначается отправителем |
|
- sock_id: uint8_t, уникальный ID сокета отправителя |
|
- only_local: uint8_t, флаг "только локальные подключения" |
|
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4) |
|
- random_padding: случайные байты до размера handshake (защита от fingerprinting) |
|
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES, |
|
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета |
|
в noencrypt_len байтах (не попадает под AES-шифрование) |
|
Инициирует новый connection для tcp instance. Если tcp instance нет (первое подключение) - создаёт. |
|
Между нодами только одно подключение, но можно добавлять каналы. |
|
|
|
2) Init подтверждение (0x03 / 0x05): |
|
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [local_link_id(1)] |
|
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2)] |
|
[random_padding...] |
|
- code: 0x03 (со сбросом) или 0x05 (без сброса) |
|
- node_id: 64 бита, big-endian |
|
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует) |
|
- mtu_local: uint16_t локальный MTU сервера |
|
- local_link_id: uint8_t идентификатор канала сервера |
|
- remote_socket_id: uint8_t (копируется из запроса) |
|
- only_local: uint8_t флаг сервера |
|
- type: uint8_t тип сокета сервера |
|
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal) |
|
- peer_port: 2 байта (порт клиента как его видит сервер) |
|
- random_padding: случайные байты |
|
|
|
3) PING (0x06): |
|
[0x06] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] |
|
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)] |
|
Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией). |
|
Используется для проверки доступности узла и NAT-тестирования. |
|
|
|
4) PONG (0x07): |
|
[0x07] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] |
|
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)] |
|
Ответ на PING. nonce копируется из запроса для сопоставления. |
|
|
|
При получении init получатель пакета должен: |
|
- reset ETCP_LINK с этим ip_port если он есть |
|
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение. |
|
приём кодограмм из UDP выглядит так: |
|
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue |
|
|
|
Keepalive: пакет без секций (только timestamp+flag_up в шифрованном заголовке, data_len=0). |
|
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика. |
|
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента). |
|
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты). |
|
|
|
Клиент: если все линки =down то начинается процедура восстановления связи: |
|
- клиент посылает init без сброса (0x04), link_state=2 |
|
- сервер как получает init проверяет session_id: |
|
- если session_id совпадает — шлёт подтверждение без сброса (0x05) |
|
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03) |
|
|
|
**** Connection session confirmation (2026-09) **** |
|
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset |
|
uses the same control protocol on every initialized UDP or TCP link, bypassing the |
|
normalizer and reliable DATA/ACK queues. A physical reset does not generate DOWN/UP. |
|
|
|
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1): |
|
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections |
|
The extra 17 bytes participate in the MTU budget. Unwrapped DATA/ACK and frames with |
|
an epoch mismatch are discarded before stream state or link liveness is updated. |
|
|
|
Control frames (all 25 bytes after timestamp/flag): |
|
0a HELLO | sender_epoch | receiver_epoch | 0 |
|
0b CHALLENGE | sender_epoch | receiver_epoch | random_cookie:u64be |
|
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be |
|
|
|
Each connection owns one pending peer epoch, fresh challenge, and retry timer. |
|
HELLO and a changed epoch advertised by INIT only request a challenge. Only a |
|
CONFIRM for the current local epoch and pending random challenge commits a peer |
|
epoch. Changing the peer epoch resets the stream but preserves our own epoch. |
|
A local fatal reset generates a new local epoch and blocks DATA until confirmation. |
|
Both sides may reset concurrently. Loss of any control frame is recovered by a |
|
500 ms retry on all initialized links. After 10 s an unconfirmed local reset closes |
|
the connection; an unconfirmed candidate alone is discarded without closing a |
|
working session. Close cancels the timer before destroying links. |
|
|
|
TCP may initially receive peer epoch zero because the server creates ETCP_CONN |
|
only after STCP handshake. It completes the same session confirmation before INIT/UP. |
|
REINIT notifies stream consumers without tearing down physical ownership. The |
|
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because |
|
its previous control messages may have been discarded by the stream reset.
|
|
|