Files
utun2/doc/etcp_protocol.txt
T

346 lines
40 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Формат кодограммы:
------------------
Кодограмма сервиса через etcp_send попадает в normalizer; его фрагменты поступают в
input_queue, затем в indexed input_send_q с назначенным seq.
Очереди, ACK/RTO, изменение окна и готовность линка вызывают etcp_conn_wake_sender.
Один call_soon обслуживает накопленные события без рекурсивного входа и задержки таймером.
При OOM выделения сетевого пакета очередь сохраняется; retry назначается через 10ms.
close/reset отменяет ожидающий запуск. Не подтверждённая сессия не передаёт DATA/ACK;
HELLO/CHALLENGE/CONFIRM и keepalive передаются своим независимым путём.
Формирование пакета:
1. Посмотреть следующий DATA и его длину, не извлекая его из input_send_q.
2. Выбрать инициализированный UP-линк с готовым shaper и местом для целого DATA:
inflight_bytes + длина DATA <= inflight_lim_bytes. Проверка выполняется без переполнения.
Среди допустимых выбирается минимум inflight_bytes, при равенстве — round-robin.
Для повтора обход начинается после last_link самого фрагмента: первый готовый другой канал,
прежний канал — последним. Чужие DATA/ACK не меняют курсор фрагмента; удаления линка обнуляют его до free.
3. Если DATA не помещается, он остаётся в очереди; для накопленных ACK выбирается
тот же линк, который принял соответствующий DATA, без ограничения DATA-cwnd/pacing.
Кумулятивный till можно отправить по любому рабочему пути. ACK-only не увеличивает нагрузку DATA-shaper;
физическая очередь канала остаётся общей, её накопление ограничивают DATA-cwnd/pacing BBR.
4. Выделить ETCP_DGRAM до удаления DATA из очереди и изменения счётчиков.
Выделить отдельную физическую попытку с новым attempt_id и зарезервировать окно выбранного линка.
Только успешное принятие транспортом фиксирует время, last_link и перенос DATA в input_wait_ack.
Ошибка send снимает резерв; логический DATA остаётся готовым без сетевого loss sample.
Пакет содержит ACK, опциональные timestamp/измерительные секции и последнюю секцию PAYLOAD.
5. В активной ограниченной burst-серии pacing обходится; DATA всё равно должен
помещаться в окно, иначе серия отправляет FILLER. ACK/FILLER окно DATA не занимают.
При приёме пакета (от etcp_connections):
- последовательно сканируем секции и отдаём их на обработку нужным обработчикам:
- ack (0x01): attempt_id подтверждает физическую попытку только принявшего ACK линка;
seq отдельно подтверждает логическую доставку. till освобождает логические записи, без BBR/снятия чужого flight.
- timestamp (0x06): обновляем last RTT, пересчитываем RTT avg10 и jitter по каждому линку; обновляем recv_dt_avg для раздельного измерения времени передачи/приёма
- [0x00] payload: сохраняем фрагмент в recv_q либо распознаём уже сохранённый/доставленный seq.
Только после этого ставим ACK с seq+attempt_id в ack_q принявшего линка; OOM до сохранения не подтверждается.
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть.
Таймер попыток каждого линка (etcp_tx.c):
- RTO = clamp(2*(data_rtt_us+data_jitter_us), 10000, 1000000) мкс.
RTT использует собственный DATA ACK минус receiver_delay; EWMA RTT=1/8, jitter=1/4 |RTT-EWMA|.
Первый RTT задаёт jitter=RTT/2. До DATA ACK используется RTT keepalive этого же пути, без него RTO=1с.
- FIFO содержит физические попытки в порядке принятия транспортом. Таймер назначается по старейшей;
при изменении RTO проверяется новый срок. Допускается оставить уже запланированную более раннюю проверку.
uasync округляет задержку вверх до миллисекунды; callback может выполниться позже расчётного срока.
- Истечение снимает только собственный физический flight и сообщает loss собственному BBR.
Если логический seq ещё не доставлен и других активных копий нет, он возвращается в input_send_q.
Уже доставленный seq не возвращается, даже если его физическая копия истекла позже.
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] — local_link_id последних попыток, только диагностика
- last_attempt_id и active_attempts — последняя отправка и число ещё учитываемых физических копий
- Payload принадлежит только логической записи. Попытка ETCP_TX_ATTEMPT хранит seq и неизменные снимки, без payload.
inflight_bytes/inflight_packets линка равны сумме DATA-длин/числу записей в его attempts, включая резерв send.
ACK/till может уже удалить логический объект, пока другая физическая копия продолжает занимать своё окно.
Повторный ACK неизвестной/истёкшей попытки не вычитает flight и не создаёт BBR sample.
ACK известного ID с другим seq отклоняется целиком; неизвестный ID допускает только логическое подтверждение seq.
Удаление линка обнуляет last_link в обеих очередях до освобождения его памяти.
Общий optimal_inflight равен сумме лимитов всех линков, с насыщением UINT32_MAX;
это дополнительный backpressure подачи новых фрагментов, не замена индивидуальных окон.
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
input_queue даже если подтверждённые записи уже удалены по selective ACK.
При заполненном sequence-окне собственный таймер запрашивает актуальный till секцией ACK_REQUEST.
Получатель отвечает через запрашивающий путь, даже без ожидающих DATA/ACK-элементов.
При продвижении frontier, reset/close таймер отменяется. Это восстанавливает поток при потере последнего till.
Память физических попыток ограничена: 16384 на линк, 32768 на соединение; исчерпание блокирует DATA до освобождения.
Очередь ACK каждого линка ограничена 16384 записями; переполнение логируется, DATA остаётся сохранённым для повтора/till.
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
BBR каждого линка получает ACK/loss samples и возвращает cwnd и pacing rate.
Скорость — доставленные байты за max(интервал отправки, интервал доставки), по снимкам при отправке.
Интервал между соседними ACK не используется: ACK одной пачки могут обрабатываться в один tick.
Внутренняя полоса — байты/мкс Q16; pacing — байты/с, BDP — байты, без повторного множителя MSS.
Минимальное окно BBR — четыре DATA-фрагмента normalizer; максимальное — bbr_max_cwnd (по умолчанию 1048576).
8192 — нижняя допустимая граница настройки bbr_max_cwnd; транспорт не повышает до неё текущее окно.
PROBE_RTT осушает flight до max(4*MSS, 0.5*BDP), выдерживает >=200мс и раунд ACK, затем восстанавливает окно.
MSS — размер DATA-фрагмента normalizer без заголовков; начальное окно UDP и STCP равно 4*MSS с верхним лимитом конфига.
В PROBE_BW действует дополнительный целевой бюджет очереди 20мс: всё окно, включая резервы ACK и PROBE_UP,
ограничивается max(4*MSS, BDP + BW*20мс) по min_rtt. STARTUP/DRAIN не ограничивают окно этой формулой,
но целый раунд RTT выше min_rtt+бюджет завершает STARTUP и переводит его в DRAIN.
Shaper списывает только DATA-байты: единицы совпадают с delivery sample и pacing BBR.
Это цель по оценкам BBR; минимум окна, изменение полосы и очереди обратного пути могут давать большую задержку.
Отказ из-за нехватки окна сохраняет cwnd_limited до следующего ACK: вычитание ACK
не должно скрывать этот факт от BBR. app_limited учитывает ожидающие очереди и
pending/частичный буфер normalizer; ограничение окном не означает отсутствие данных.
Потеря для BBR учитывается при RTO, фактическая повторная передача — при новой попытке.
При уменьшении окна уже отправленные пакеты остаются в учёте; новые DATA ждут ACK/RTO.
Измерительные секции burst 0x07/0x08 и FILLER поддерживаются реализацией.
**** Формат кодограмм для 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 вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
Для DATA/ACK после этих 3 байт добавляется обязательный заголовок сессии:
[0x09][sender_epoch(8 BE)][receiver_epoch(8 BE)], затем идут секции из etcp.c.
Итого шифруется: 3 + 17 + data_len байт для DATA/ACK;
для остальных кодограмм — 3 + (data_len - noencrypt_len) байт.
Формат и подтверждение эпох описаны в разделе «Подтверждение транспортной сессии» ниже.
2. Опциональные секции (одна или несколько) добавляются в etcp.c внутри DATA/ACK после заголовка сессии:
а) Подтверждения (ACK) - заголовок 0x01:
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][attempt_id(8 LE)][delay_us(4 LE)] × count]
- count: количество ACK записей (1 байт; до 255, дополнительно ограничено MTU и оставшимся местом)
- last_delivered_id: uint32_t — последний ID, доставленный получателю (по этот ID всё собрано подряд)
- rx_dup_count: uint16_t — счётчик принятых дубликатов (локальный, для вычисления tx_dup_count на удалённой стороне)
- seq: uint32_t — номер подтверждаемого пакета
- attempt_id: uint64_t — ID конкретной физической попытки DATA; возвращается только тем же путём
- delay_us: uint32_t — задержка между приёмом DATA и формированием ACK, мкс; UINT32_MAX исключает RTT sample
Каждая запись = 16 байт, база ACK = 8 байт; count=0 допустим для передачи till.
ACK_REQUEST: [0x0a], одна секция длиной 1 байт, запрос повторной отправки актуального till.
в) 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 (был принят пакет с момента последней отправки)
и если прошло менее 100с (dt < 1000000 таймбаз). Секция не является источником DATA RTT для BBR/RTO.
3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя):
[0x00] [ID 4 байта LE] [attempt_id 8 байт LE] [данные...]
- ID: uint32_t, 32-битный циклический номер пакета
- attempt_id: uint64_t, монотонный ненулевой ID; не сбрасывается при reset или пересоздании линка.
Переполнение закрывает соединение. Заголовок PAYLOAD занимает 13 байт; при повторе attempt_id новый, seq прежний.
Пакет может содержать несколько опциональных секций (например, ACK + channel_timestamp + данные),
Порядок: ACK, возможный ACK_REQUEST, измерительные/временные секции и FILLER, затем payload последним.
Разбор секций — последовательное сканирование с известными размерами (нет явного поля длины секции).
цель протокола:
1. контролировать состояние очередей (не накапливать данные где этого можно избежать), анализировать RTT и inflight - динамически подстраивать размер окна и ретрансмиссии, утилизируя сеть насколько это возможно но не допуская наполнения буферов в сети (что приводит к бестолковому увеличению RTT)
Установка подключения для канала (реализуется в etcp_connections.c/h):
каждое ETCP подключение может содержать несколько линков до endpoint (разные маршруты, каналы связи).
При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно):
- RTT последнего пакета и время отправки последнего пакета
- общее количество отправленных/принятых байт
- количество перезапросов (по каждому каналу отдельно) в пакетах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики.
- обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент
Планировщик/балансир каналов (etcp_loadbalancer.c/h):
для каждого канала ограничение полосы с тестированием отклика:
- временную нагрузку shaper считаем по data_len + 2 байта на пакет (текущая реализация).
Ограничение реализуем следующим образом:
- в структуре есть счётчик времени (квантизация стандартная 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 собственный reset_id (64 бита, BE); ответ не повторяет эпоху запроса.
- первая неизвестная эпоха пира сохраняется из handshake. Смена уже известной эпохи запускает challenge;
только подходящий CONFIRM сбрасывает поток и заменяет эпоху пира, сохраняя собственную.
- повтор INIT и коллизия встречных подключений сами по себе не сбрасывают поток.
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT_REQUEST (0x02);
если link_state=2 (reconnect) — INIT_REQUEST_NOINIT (0x04). Коды INIT не заменяют подтверждение смены эпохи.
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 (начальный handshake линка)
0x03 — ETCP_INIT_RESPONSE (вариант ответа INIT)
0x04 — ETCP_INIT_REQUEST_NOINIT (восстановление линка, также разрешение коллизии)
0x05 — ETCP_INIT_RESPONSE_NOINIT (вариант ответа INIT)
0x06 — ETCP_PING (проверка доступности)
0x07 — ETCP_PONG (ответ на пинг)
0x08 — ETCP_KEEPALIVE
0x09 — ETCP_SESSION_DATA (обёртка секций DATA/ACK с эпохами)
0x0a — ETCP_SESSION_HELLO
0x0b — ETCP_SESSION_CHALLENGE
0x0c — ETCP_SESSION_CONFIRM
0x0d — ETCP_TCP_ADDRESS (STCP: code + family(4/6) + observed IP(16), без порта)
Варианты ответа 0x03/0x05 не определяют смену эпохи: клиент в обоих случаях обрабатывает reset_id одинаково.
1) Init запрос (0x02 / 0x04):
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [keepalive(2 BE)]
[recovery(2 BE)] [link_id(1)] [socket_id(1)] [only_local(1)] [type(1)]
[src_ipv4(4)] [src_port(2 BE)] [collision(1)] [ed25519_pubkey(32)] [device_type(1)]
[random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Фиксированное тело до padding — 67 байт (ETCP_INIT_REQUEST_PKT).
- code: 0x02 или 0x04, вариант handshake линка
- node_id: 64 бита, идентификатор узла (big-endian)
- reset_id: uint64_t, собственная локальная эпоха потока клиента (big-endian)
- mtu: uint16_t локальный MTU
- keepalive: uint16_t интервал keepalive в мс
- recovery: uint16_t = link->recovery_interval/100 (исходная единица — 0.1 мс)
- link_id: uint8_t (0-255), назначается отправителем
- socket_id: uint8_t, уникальный ID сокета отправителя
- only_local: uint8_t, флаг "только локальные подключения"
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
- src_ipv4/src_port: адрес интерфейса отправителя; для IPv6 src_ipv4=0, порт сохраняется;
при SOCKS5 оба поля нулевые. Это заявленный адрес, а не наблюдение внешнего NAT-адреса.
- collision: 1 — отправитель разрешает встречное подключение, заявляя роль master для линка
- ed25519_pubkey: публичный ключ Ed25519 отправителя, 32 байта
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
- random_padding: случайные байты до размера handshake (защита от fingerprinting)
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES,
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета
в noencrypt_len байтах (не попадает под AES-шифрование)
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
2) Init подтверждение (0x03 / 0x05):
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [link_id(1)]
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2 BE)]
[ed25519_pubkey(32)] [device_type(1)] [keepalive(2 BE)] [random_padding...]
Фиксированное тело до padding — 64 байта (ETCP_INIT_RESPONSE_PKT).
- code: 0x03 или 0x05, вариант ответа INIT; сам по себе не сбрасывает поток клиента
- node_id: 64 бита, big-endian
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
- mtu: uint16_t локальный MTU сервера
- link_id: uint8_t идентификатор канала сервера
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
- only_local: uint8_t флаг сервера
- type: uint8_t тип сокета сервера
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер, в сетевом порядке)
- peer_port: uint16_t (порт клиента как его видит сервер, big-endian)
При IPv6 оба поля нулевые.
- ed25519_pubkey: публичный ключ Ed25519 сервера, 32 байта
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
- keepalive: uint16_t интервал keepalive сервера в мс
- random_padding: случайные байты
3) PING (0x06):
[0x06] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [user_data_len(2 BE)] [user_data...] [padding...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Отправляется с отдельным crypto-контекстом (не связан с ETCP-сессией).
Используется для проверки доступности узла и NAT-тестирования.
4) PONG (0x07):
[0x07] [flags(1)] [node_id(8 BE)] [nonce(8 BE)] [resp_data_len(2 BE)] [response_data...] [padding...]
[salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
Ответ на PING. nonce копируется из запроса для сопоставления.
Nonce генерируется CSPRNG. Проверяются также ключ отправителя, source IP:port,
ID и поколение принимающего сокета. Флаг OBSERVE (0x04) запрашивает ответ
address(19) || tag(25): family(4/6), IP(16), port(2, BE), NAT2UDP!(8),
socket_id(1), generation(8, BE), round(8, BE).
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
При получении INIT получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
через etcp_conn_apply_peer_reset_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: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
Padding keepalive, PING/PONG и STCP-handshake:
- ETCP_CONN хранит собственный диапазон min/max и независимый таймер для всех своих линков.
- UTUN_INSTANCE хранит отдельный диапазон и таймер для PING/PONG без существующего ETCP_CONN.
- При создании владельца и каждом обновлении: min=rand(0,30), max=min+rand(32,128), границы включительные.
- Следующее обновление назначается через rand(50,300) секунд. При закрытии владельца таймер отменяется.
- Каждый пакет независимо выбирает длину rand(min,max); padding заполняется нулями ДО шифрования.
- UDP PING/PONG: padding шифруется перед CRC/tag; открытый salt+pubkey остаётся в конце датаграммы.
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance.
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
После handshake CTR непрерывно продолжается через padding и DATA, без повторной инициализации на кадр.
- Padding не должен превышать 158 байт. Для UDP длина ограничивается оставшимся MTU;
если не помещается даже min, отправка завершается ошибкой без обрезания пользовательских данных.
- Диагностика диапазонов/обновлений/PING/PONG: etcp=debug; keepalive: keepalive=debug.
Клиент: если все линки =down то начинается процедура восстановления связи:
- клиент посылает init без сброса (0x04), link_state=2
- сервер восстанавливает физический линк и отвечает INIT_RESPONSE/INIT_RESPONSE_NOINIT со своей эпохой.
- прежняя эпоха пира не меняет поток; смена известной эпохи подтверждается через CHALLENGE/CONFIRM.
Код ответа 0x03/0x05 не является подтверждением смены эпохи.
**** Подтверждение транспортной сессии (etcp_session.c/h) ****
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 stream reset does not generate physical 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.
The first nonzero peer epoch from a physical handshake is stored directly if unknown.
HELLO and a change to an already known 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.
Отказы памяти и прерывание потока:
- OOM до сохранения RX DATA в recv_q не создаёт ACK; отправитель повторяет DATA.
- ACK означает принятие в текущий транспортный поток, а успешный etcp_send —
передачу владения транспорту. Оба не гарантируют завершение операции сервиса
после прерывания сессии. Надёжность операции через REINIT обеспечивает её владелец.
- OOM после принятия данных в normalizer или при подготовке TX inflight вызывает
fatal REINIT: новая локальная эпоха, блокировка DATA, отбрасывание старых очередей,
частичных кодограмм и физических попыток. Старые payload не воспроизводятся.
Физические линки сохраняются; очистка очередей/reset не выделяет память.
- Активный обработчик освобождает собственный извлечённый фрагмент ровно один раз;
после reset/close из callback он прекращает разбор старой кодограммы/пакета.
- Согласование новой эпохи выполняется после очистки. OOM control-пакета оставляет
повтор активным, без продления исходного срока 10с. Отказ обязательного session-
таймера или генерации локальной эпохи закрывает соединение с диагностикой.
Если невозможно выделить даже обязательный deferred-close token внутри callback,
процесс завершается через abort: соединение не оставляется в зависшем состоянии.
- После подтверждения эпох владельцы получают REINIT и восстанавливают операции
своего протокола; при невозможности согласования соединение закрывается.