379 lines
46 KiB
Plaintext
379 lines
46 KiB
Plaintext
Формат кодограммы:
|
||
------------------
|
||
Кодограмма сервиса через 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_no для этого seq и зарезервировать окно выбранного линка.
|
||
Только успешное принятие транспортом фиксирует время, 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): пара seq+attempt_no подтверждает физическую попытку только принявшего ACK линка;
|
||
seq отдельно подтверждает логическую доставку. till освобождает логические записи, без BBR/снятия чужого flight.
|
||
- timestamp (0x06): обновляем last RTT, пересчитываем RTT avg10 и jitter по каждому линку; обновляем recv_dt_avg для раздельного измерения времени передачи/приёма
|
||
- [0x00] payload: сохраняем фрагмент в recv_q либо распознаём уже сохранённый/доставленный seq.
|
||
Только после этого ставим ACK с seq+attempt_no в ack_q принявшего линка; OOM до сохранения не подтверждается.
|
||
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть.
|
||
|
||
Таймер попыток каждого линка (etcp_tx.c):
|
||
- RTO = clamp(ceil(1.2*data_rtt_us + 1.5*data_jitter_us + 3000), 10000, 1000000) мкс.
|
||
Целочисленный расчёт использует uint64 и округляет вверх до целой микросекунды.
|
||
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 — внутренний составной ключ (seq<<16)|attempt_no последней принятой отправки
|
||
- attempt_count — выданные номера попыток данного seq, общие для всех каналов, включая неудачные send
|
||
- 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.
|
||
ACK с подтверждениями также возвращает точку приёма своего пути: накопленные DATA-байты и монотонное время
|
||
последнего увеличения счётчика. В счётчик входят допустимые сохранённые/уже доставленные DATA, включая копии;
|
||
OOM до сохранения и отвергнутые DATA его не меняют. Считаются байты полезной нагрузки секции, без заголовков.
|
||
При отправке попытка запоминает известную точку. ACK действующей попытки продвигает точку пути;
|
||
повторный/чужой ID её не меняет, переставленные ACK не откатывают. Противоречивая пара логируется и игнорируется.
|
||
При положительных дельтах добавляется нижняя граница интервала ceil(delivered*delta_rx_us/delta_rx_bytes).
|
||
Итоговый interval — максимум трёх границ; BW может только уменьшиться относительно прежнего sample.
|
||
Счётчики и времена — uint64, часы двух сторон не сравниваются. Нулевые/неизвестные дельты не создают границы.
|
||
Если интервал не представим в uint32, sample не используется для BW вместо насыщения, завышающего скорость.
|
||
После пустого физического flight база начинается заново, чтобы простой не занижал скорость следующей передачи.
|
||
Reset потока обнуляет точки приёма; исчерпание локального 64-битного счётчика закрывает соединение с ERROR.
|
||
RTT/min_rtt, формула RTO, политика окна и дедлайн ACK не корректируются приёмной точкой.
|
||
Внутренняя полоса — байты/мкс 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_no(2 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_no: uint16_t — номер физической попытки данного seq, 1..65535; возвращается только тем же путём
|
||
- delay_us: uint32_t — задержка между приёмом DATA и формированием ACK, мкс; UINT32_MAX исключает RTT sample
|
||
При count>0 после всех записей обязательно добавляется [rx_data_bytes(8 LE)][rx_data_time_us(8 LE)].
|
||
Это точка приёма всего пути на момент формирования ACK, а не отдельной подтверждаемой попытки.
|
||
Время — монотонные микросекунды получателя при последнем положительном приросте DATA-байтов; 0/0 — точки нет.
|
||
Каждая запись = 10 байт, база ACK = 8 байт; полный размер = 8+10*count+(count>0 ? 16 : 0).
|
||
count=0 допустим для передачи till, точки приёма не содержит. До обработки проверяется вся длина ACK.
|
||
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_no 2 байта LE] [данные...]
|
||
- ID: uint32_t, номер фрагмента 1..UINT32_MAX в пределах одной эпохи потока.
|
||
- attempt_no: uint16_t, номер 1..65535, общий для всех каналов одного ID.
|
||
Номер выдаётся при создании попытки и не возвращается при отказе send; исходный seq при повторе сохраняется.
|
||
После номера65535 следующая попытка аварийно меняет эпоху через REINIT; счётчик не оборачивается.
|
||
Перед выдачей seq=0 после UINT32_MAX поток также меняет эпоху: пара seq/attempt_no не повторяется внутри эпохи.
|
||
Смена эпохи отбрасывает прежний поток, владельцы восстанавливают свои операции по REINIT, как при фатальном OOM.
|
||
Старые DATA/ACK отклоняются заголовком сессии до разбора. Заголовок PAYLOAD занимает7 байт.
|
||
Внутренние хеш-очереди сохраняют64-битный ключ (seq<<16)|attempt_no; он не передаётся по сети целиком.
|
||
Пакет может содержать несколько опциональных секций (например, 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, без немедленного сброса потока при смене известной эпохи.
|
||
Принятый INIT обновляет recv_keepalive, время последнего RX и входной remote_keepalive=flag_up
|
||
до переиспользования буфера для ответа. INIT с flag_up=0 снимает прежний UP UDP-линка:
|
||
получение запроса доказывает только входящую достижимость, обратный путь ещё не подтверждён.
|
||
Ответ и keepalive отправляются и при DOWN, чтобы холодное подключение и восстановление
|
||
могли завершить двустороннюю проверку. При DOWN одного линка соединение остаётся UP,
|
||
если есть другой работоспособный линк. Повтор неизменного состояния не создаёт нового DOWN/UP.
|
||
INIT_RESPONSE должен содержать полное обязательное тело (64 байта без timestamp/flag_up)
|
||
и node_id, соответствующий аутентифицированному ключу пира. Усечённый ответ или чужой node_id
|
||
отклоняются до изменения живости, эпохи и параметров линка. Дополнительный padding разрешён.
|
||
приём кодограмм из 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 и восстанавливают операции
|
||
своего протокола; при невозможности согласования соединение закрывается.
|